hexo-theme-stellar 1.37.0 → 1.39.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/.claude/skills/stellar-theme-dev/SKILL.md +50 -0
- package/CHANGELOG.md +75 -0
- package/CONTRIBUTING.md +155 -0
- package/README.md +56 -35
- package/_config.yml +16 -13
- package/_data/icons.yml +3 -0
- package/languages/en.yml +37 -0
- package/languages/zh-CN.yml +37 -0
- package/languages/zh-TW.yml +37 -0
- package/layout/_partial/comments/layout.ejs +2 -1
- package/layout/_partial/main/navbar/article_banner.ejs +11 -5
- package/layout/_partial/main/navbar/nav_tabs_blog.ejs +2 -1
- package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +1 -0
- package/layout/_partial/main/notebook/paginator.ejs +3 -2
- package/layout/_partial/main/pin_slider.ejs +325 -0
- package/layout/_partial/main/post_list/paginator.ejs +4 -3
- package/layout/_partial/main/post_list/post_card.ejs +2 -4
- package/layout/_partial/scripts/defines.ejs +2 -1
- package/layout/_partial/scripts/lazyload.ejs +24 -0
- package/layout/_partial/scripts/utils.ejs +58 -15
- package/layout/_partial/sidebar/grad-def.ejs +26 -0
- package/layout/_partial/sidebar/index_leftbar.ejs +25 -10
- package/layout/_partial/sidebar/logo.ejs +22 -1
- package/layout/_partial/sidebar/menu.ejs +11 -5
- package/layout/_partial/widgets/components/linklist.ejs +2 -2
- package/layout/_partial/widgets/toc.ejs +12 -8
- package/layout/_plugins/scrollreveal.ejs +17 -2
- package/layout/index.ejs +36 -1
- package/layout/layout.ejs +1 -1
- package/layout/notebooks.ejs +3 -1
- package/layout/notes.ejs +3 -1
- package/package.json +5 -2
- package/scripts/tags/index.js +1 -0
- package/scripts/tags/lib/mbti.js +169 -0
- package/source/css/_common/canonical.styl +9 -10
- package/source/css/_common/device.styl +12 -12
- package/source/css/_common/highlight.styl +0 -2
- package/source/css/_common/html.styl +2 -1
- package/source/css/_common/svg.styl +9 -1
- package/source/css/_components/layout.styl +11 -8
- package/source/css/_components/list.styl +55 -21
- package/source/css/_components/main.styl +2 -2
- package/source/css/_components/md.styl +0 -1
- package/source/css/_components/pages/article-story.styl +1 -3
- package/source/css/_components/partial/article-banner.styl +17 -11
- package/source/css/_components/partial/article-footer.styl +0 -1
- package/source/css/_components/partial/navbar.styl +10 -27
- package/source/css/_components/partial/paginator.styl +9 -11
- package/source/css/_components/pin-slider.styl +240 -0
- package/source/css/_components/sidebar/footer.styl +1 -1
- package/source/css/_components/sidebar/logo.styl +27 -1
- package/source/css/_components/sidebar/menu.styl +53 -30
- package/source/css/_components/sidebar/nav-area.styl +1 -1
- package/source/css/_components/sidebar/search.styl +50 -11
- package/source/css/_components/sidebar/sidebar.styl +32 -5
- package/source/css/_components/tag-plugins/banner.styl +2 -1
- package/source/css/_components/tag-plugins/chat.styl +3 -1
- package/source/css/_components/tag-plugins/folding.styl +1 -2
- package/source/css/_components/tag-plugins/friends.styl +1 -0
- package/source/css/_components/tag-plugins/friends_posts.styl +2 -1
- package/source/css/_components/tag-plugins/gallery.styl +1 -2
- package/source/css/_components/tag-plugins/grid.styl +1 -2
- package/source/css/_components/tag-plugins/image.styl +0 -2
- package/source/css/_components/tag-plugins/link.styl +0 -1
- package/source/css/_components/tag-plugins/mbti.styl +80 -0
- package/source/css/_components/tag-plugins/note.styl +1 -2
- package/source/css/_components/tag-plugins/okr.styl +1 -2
- package/source/css/_components/tag-plugins/sites.styl +0 -1
- package/source/css/_components/tag-plugins/timeline.styl +8 -6
- package/source/css/_components/widgets/components.styl +19 -7
- package/source/css/_components/widgets/ghuser.styl +3 -2
- package/source/css/_components/widgets/list.styl +20 -8
- package/source/css/_components/widgets/markdown.styl +5 -2
- package/source/css/_components/widgets/related.styl +6 -4
- package/source/css/_components/widgets/timeline.styl +1 -1
- package/source/css/_components/widgets/toc.styl +9 -6
- package/source/css/_components/widgets/widgets.styl +11 -10
- package/source/css/_custom.styl +5 -2
- package/source/css/_defines/const.styl +2 -2
- package/source/css/_defines/func.styl +49 -3
- package/source/css/_defines/theme_base.styl +22 -1
- package/source/css/_plugins/comments/artalk.styl +2 -2
- package/source/css/_plugins/comments/twikoo.styl +0 -2
- package/source/css/_plugins/tianli_gpt.styl +3 -0
- package/source/js/main.js +39 -17
- package/source/js/services/artalk_latest_comment.js +43 -1
- package/source/js/services/memos.js +12 -3
|
@@ -6,7 +6,7 @@ if (cmt.service) {
|
|
|
6
6
|
cmt[cmt.service] = Object.assign({}, theme.comments[cmt.service]);
|
|
7
7
|
}
|
|
8
8
|
var loadComment = false;
|
|
9
|
-
if (
|
|
9
|
+
if (cmt.service && cmt.service.length > 0) {
|
|
10
10
|
if (page.comments == undefined || page.comments != false) {
|
|
11
11
|
loadComment = true;
|
|
12
12
|
}
|
|
@@ -34,6 +34,7 @@ if (cmt.service && page[cmt.service]) {
|
|
|
34
34
|
Object.assign(cmt[cmt.service], page[cmt.service]);
|
|
35
35
|
}
|
|
36
36
|
page.cmt = cmt;
|
|
37
|
+
page.cmt_rendered = loadComment;
|
|
37
38
|
%>
|
|
38
39
|
<% if (loadComment) { %>
|
|
39
40
|
<div class="related-wrap md-text<%- scrollreveal(' ') %>" id="comments">
|
|
@@ -105,23 +105,29 @@ function layoutDiv() {
|
|
|
105
105
|
return ``
|
|
106
106
|
}
|
|
107
107
|
const top = layoutBreadcrumb()
|
|
108
|
-
var
|
|
108
|
+
var bannerStyle = ``
|
|
109
|
+
var contentStyle = ``
|
|
109
110
|
var el = ``
|
|
110
|
-
|
|
111
|
+
if (banner.url?.length > 0) {
|
|
112
|
+
bannerStyle += ` style="--bg-url:url('${banner.url.replace(/'/g, '%27')}')"`
|
|
113
|
+
}
|
|
114
|
+
el += `<div class="article-banner-wrap${scrollreveal(' ')}">`
|
|
115
|
+
el += `<div class="article banner top"${bannerStyle}>`
|
|
111
116
|
if (banner.url?.length > 0) {
|
|
112
117
|
el += `<img class="lazy bg" data-src="${banner.url}">`
|
|
113
118
|
if (banner.color) {
|
|
114
|
-
|
|
119
|
+
contentStyle += ' style="--text-banner:' + banner.color + '"'
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
el += `
|
|
118
|
-
<div class="content"${
|
|
123
|
+
<div class="content"${contentStyle}>
|
|
119
124
|
${top}
|
|
120
125
|
${bottom}
|
|
121
126
|
</div>
|
|
122
127
|
`
|
|
123
128
|
el += `</div>`
|
|
129
|
+
el += `</div>`
|
|
124
130
|
return el
|
|
125
131
|
}
|
|
126
132
|
%>
|
|
127
|
-
<%- layoutDiv() %>
|
|
133
|
+
<%- layoutDiv() %>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%
|
|
2
2
|
function layoutDiv() {
|
|
3
3
|
var el = '';
|
|
4
|
+
el += partial('_partial/main/pin_slider', {type: 'post'});
|
|
4
5
|
el += '<div class="navbar top">';
|
|
5
6
|
el += '<div class="navbar-blur">';
|
|
6
7
|
el += '<div class="navbar-container">';
|
|
@@ -68,4 +69,4 @@ function layoutDiv() {
|
|
|
68
69
|
}
|
|
69
70
|
%>
|
|
70
71
|
|
|
71
|
-
<%- layoutDiv() %>
|
|
72
|
+
<%- layoutDiv() %>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<% if ((page.total || 0) > 1) { %>
|
|
2
2
|
<div class="paginator-wrap dis-select<%= scrollreveal(' ') %>">
|
|
3
3
|
<%- paginator({
|
|
4
|
-
prev_text: '',
|
|
5
|
-
next_text: '',
|
|
4
|
+
prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
|
|
5
|
+
next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
|
|
6
6
|
force_prev_next: true,
|
|
7
|
+
escape: false,
|
|
7
8
|
}) %>
|
|
8
9
|
</div>
|
|
9
10
|
<% } %>
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
<%
|
|
2
|
+
// 置顶内容轮播:所有带 navbar top 的列表页上方共用。
|
|
3
|
+
// type: post / wiki / topic
|
|
4
|
+
var pinType = type || 'post'
|
|
5
|
+
var pinItems = []
|
|
6
|
+
|
|
7
|
+
function pinValue(item) {
|
|
8
|
+
return item.pin != null ? item.pin : item.sticky
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 只要设置了 pin(含 0、负数)即视为置顶,未设置 / false 不置顶
|
|
12
|
+
function isPinned(item) {
|
|
13
|
+
var v = pinValue(item)
|
|
14
|
+
return v !== false && v != null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 排序仅比较数值大小,true 视作 1
|
|
18
|
+
function pinWeight(item) {
|
|
19
|
+
var v = pinValue(item)
|
|
20
|
+
if (v === true) {
|
|
21
|
+
return 1
|
|
22
|
+
}
|
|
23
|
+
var n = Number(v)
|
|
24
|
+
return isNaN(n) ? 0 : n
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (pinType == 'wiki') {
|
|
28
|
+
var shelf = theme.wiki?.shelf || []
|
|
29
|
+
for (var wi = 0; wi < shelf.length; wi++) {
|
|
30
|
+
var wproj = theme.wiki.tree[shelf[wi]]
|
|
31
|
+
if (wproj != null && isPinned(wproj)) {
|
|
32
|
+
pinItems.push(wproj)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
var allPosts = site.posts.toArray() || []
|
|
37
|
+
for (var pi = 0; pi < allPosts.length; pi++) {
|
|
38
|
+
var post = allPosts[pi]
|
|
39
|
+
if (post.indexing === false) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (isPinned(post)) {
|
|
43
|
+
pinItems.push(post)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pinItems.sort(function(a, b) {
|
|
49
|
+
return pinWeight(b) - pinWeight(a)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// 文章幻灯片:固定「标题 + 一行小字」结构,宽高比统一取自 pin_slider.ratio。
|
|
53
|
+
// 标题:poster.headline > title;小字:poster.caption > description > excerpt(截断 50 字)。
|
|
54
|
+
// 样式与 poster 卡片 cover-info 一致;无封面时为纯白卡片(文字按普通文章颜色)。
|
|
55
|
+
function postSubtitle(post) {
|
|
56
|
+
if (post.poster != null && post.poster.caption != null && post.poster.caption.length > 0) {
|
|
57
|
+
return post.poster.caption
|
|
58
|
+
}
|
|
59
|
+
if (post.description != null && post.description.length > 0) {
|
|
60
|
+
return post.description
|
|
61
|
+
}
|
|
62
|
+
var text = post.excerpt || post.content || ''
|
|
63
|
+
text = strip_html(text).replace(/\s+/g, ' ').trim()
|
|
64
|
+
return text.length > 0 ? truncate(text, {length: 50}) : ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function postSlide(post) {
|
|
68
|
+
var noCover = !(post.cover != null && post.cover.length > 0)
|
|
69
|
+
var headline = (post.poster != null && post.poster.headline != null && post.poster.headline.length > 0) ? post.poster.headline : post.title
|
|
70
|
+
var subtitle = postSubtitle(post)
|
|
71
|
+
var el = ''
|
|
72
|
+
el += '<a class="pin-slide post-slide' + (noCover ? ' no-cover' : '') + '" href="' + pretty_url(post.link || post.path) + '"'
|
|
73
|
+
if (!noCover) {
|
|
74
|
+
el += ' style="--pin-cover-url:url(\'' + post.cover.replace(/'/g, '%27') + '\')"'
|
|
75
|
+
}
|
|
76
|
+
el += '>'
|
|
77
|
+
if (!noCover) {
|
|
78
|
+
el += '<img class="pin-slide-bg" src="' + post.cover + '" alt=""/>'
|
|
79
|
+
}
|
|
80
|
+
el += '<div class="pin-slide-text">'
|
|
81
|
+
el += '<div class="pin-slide-headline">' + headline + '</div>'
|
|
82
|
+
if (subtitle.length > 0) {
|
|
83
|
+
el += '<div class="pin-slide-caption">' + subtitle + '</div>'
|
|
84
|
+
}
|
|
85
|
+
el += '</div>'
|
|
86
|
+
el += '</a>'
|
|
87
|
+
return el
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function projectChips(tags) {
|
|
91
|
+
var caps = ''
|
|
92
|
+
if (tags && tags.length > 0) {
|
|
93
|
+
caps += '<span class="pin-slide-chip">' + tags.join('</span><span class="pin-slide-chip">') + '</span>'
|
|
94
|
+
}
|
|
95
|
+
return caps
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wikiSlide(proj) {
|
|
99
|
+
var img = proj.cover || proj.icon || theme.default.project
|
|
100
|
+
var title = proj.title || proj.name || proj.id
|
|
101
|
+
var excerpt = proj.description || ''
|
|
102
|
+
var el = ''
|
|
103
|
+
el += '<a class="pin-slide" href="' + pretty_url(proj.homepage?.path || '/') + '">'
|
|
104
|
+
el += '<img class="pin-slide-bg" src="' + img + '" alt=""/>'
|
|
105
|
+
el += '<div class="pin-slide-mask"></div>'
|
|
106
|
+
el += '<div class="pin-slide-info">'
|
|
107
|
+
el += '<div class="pin-slide-caps">' + projectChips(proj.tags) + '</div>'
|
|
108
|
+
el += '<div class="pin-slide-title">' + title + '</div>'
|
|
109
|
+
if (excerpt.length > 0) {
|
|
110
|
+
el += '<div class="pin-slide-excerpt">' + excerpt + '</div>'
|
|
111
|
+
}
|
|
112
|
+
el += '</div>'
|
|
113
|
+
el += '</a>'
|
|
114
|
+
return el
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (pinItems.length > 0) {
|
|
118
|
+
var slidesHtml = ''
|
|
119
|
+
var dotsHtml = ''
|
|
120
|
+
for (var si = 0; si < pinItems.length; si++) {
|
|
121
|
+
var item = pinItems[si]
|
|
122
|
+
if (pinType == 'wiki') {
|
|
123
|
+
slidesHtml += wikiSlide(item)
|
|
124
|
+
} else {
|
|
125
|
+
slidesHtml += postSlide(item)
|
|
126
|
+
}
|
|
127
|
+
if (pinItems.length > 1) {
|
|
128
|
+
var label = item.title || item.name || item.id || (si + 1)
|
|
129
|
+
dotsHtml += '<button type="button" class="pin-slider-dot' + (si == 0 ? ' active' : '') + '" data-index="' + si + '" aria-label="' + label + '"></button>'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
var groupKey = pinType == 'wiki' ? 'wiki' : 'post'
|
|
133
|
+
var sliderInterval = 5000 // 自动轮播间隔固定 5000ms
|
|
134
|
+
var el = ''
|
|
135
|
+
el += '<div class="pin-slider" id="pin-slider" data-interval="' + sliderInterval + '" data-key="' + pinType + '" data-group="' + groupKey + '">'
|
|
136
|
+
el += '<div class="pin-slider-track">'
|
|
137
|
+
el += slidesHtml
|
|
138
|
+
el += '</div>'
|
|
139
|
+
if (dotsHtml.length > 0) {
|
|
140
|
+
el += '<div class="pin-slider-dots">' + dotsHtml + '</div>'
|
|
141
|
+
el += '<button type="button" class="pin-slider-nav prev">' + icon('solar:double-alt-arrow-left-bold-duotone') + '</button>'
|
|
142
|
+
el += '<button type="button" class="pin-slider-nav next">' + icon('solar:double-alt-arrow-right-bold-duotone') + '</button>'
|
|
143
|
+
}
|
|
144
|
+
el += '</div>'
|
|
145
|
+
%>
|
|
146
|
+
<%- el %>
|
|
147
|
+
<script>
|
|
148
|
+
function initWhenReady() {
|
|
149
|
+
if (typeof utils !== 'undefined' && utils.initPlugin) {
|
|
150
|
+
utils.initPlugin(initPinSlider, 'pin-slider');
|
|
151
|
+
} else {
|
|
152
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
153
|
+
if (typeof utils !== 'undefined' && utils.initPlugin) {
|
|
154
|
+
utils.initPlugin(initPinSlider, 'pin-slider');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
initWhenReady();
|
|
160
|
+
function initPinSlider() {
|
|
161
|
+
var slider = document.getElementById('pin-slider');
|
|
162
|
+
if (!slider) return;
|
|
163
|
+
var track = slider.querySelector('.pin-slider-track');
|
|
164
|
+
var slides = slider.querySelectorAll('.pin-slide');
|
|
165
|
+
var count = slides.length;
|
|
166
|
+
if (!track || count <= 1) {
|
|
167
|
+
slider.style.visibility = 'visible';
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
var interval = parseInt(slider.getAttribute('data-interval'), 10) || 4000;
|
|
171
|
+
var type = slider.getAttribute('data-key') || 'post';
|
|
172
|
+
var group = slider.getAttribute('data-group') || 'post';
|
|
173
|
+
var contentKey = type + '::' + Array.prototype.map.call(slides, function(a) {
|
|
174
|
+
var link = a.querySelector('a.post-card, a.pin-slide');
|
|
175
|
+
return link ? link.getAttribute('href') : a.getAttribute('href');
|
|
176
|
+
}).join('|');
|
|
177
|
+
var index = restorePinIndex(group, contentKey, count);
|
|
178
|
+
var reduced = false;
|
|
179
|
+
try {
|
|
180
|
+
reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
181
|
+
} catch (e) {}
|
|
182
|
+
slider.style.setProperty('--pin-duration', interval + 'ms');
|
|
183
|
+
if (reduced) {
|
|
184
|
+
slider.classList.add('reduced');
|
|
185
|
+
}
|
|
186
|
+
function goTo(n) {
|
|
187
|
+
index = (n + count) % count;
|
|
188
|
+
track.style.transform = 'translateX(-' + (index * 100) + '%)';
|
|
189
|
+
var dots = slider.querySelector('.pin-slider-dots');
|
|
190
|
+
var items = dots ? dots.children : [];
|
|
191
|
+
for (var i = 0; i < count; i++) {
|
|
192
|
+
if (!items[i]) continue;
|
|
193
|
+
if (i === index) {
|
|
194
|
+
items[i].classList.add('active');
|
|
195
|
+
items[i].setAttribute('aria-current', 'true');
|
|
196
|
+
} else {
|
|
197
|
+
items[i].classList.remove('active');
|
|
198
|
+
items[i].removeAttribute('aria-current');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
savePinIndex(group, contentKey, index, count);
|
|
202
|
+
}
|
|
203
|
+
function next() { goTo(index + 1); }
|
|
204
|
+
function prev() { goTo(index - 1); }
|
|
205
|
+
function start() {
|
|
206
|
+
if (reduced) return;
|
|
207
|
+
slider.classList.remove('paused');
|
|
208
|
+
}
|
|
209
|
+
function stop() {
|
|
210
|
+
slider.classList.add('paused');
|
|
211
|
+
}
|
|
212
|
+
function onVis() {
|
|
213
|
+
if (document.hidden) {
|
|
214
|
+
stop();
|
|
215
|
+
} else {
|
|
216
|
+
start();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
var startX = null;
|
|
220
|
+
var startY = null;
|
|
221
|
+
function onTouchStart(e) {
|
|
222
|
+
stop();
|
|
223
|
+
var t = e.changedTouches[0];
|
|
224
|
+
startX = t.clientX;
|
|
225
|
+
startY = t.clientY;
|
|
226
|
+
}
|
|
227
|
+
function onTouchEnd(e) {
|
|
228
|
+
if (startX == null) return;
|
|
229
|
+
var t = e.changedTouches[0];
|
|
230
|
+
var dx = t.clientX - startX;
|
|
231
|
+
var dy = t.clientY - startY;
|
|
232
|
+
startX = null;
|
|
233
|
+
startY = null;
|
|
234
|
+
if (Math.abs(dx) > 30 && Math.abs(dx) > Math.abs(dy)) {
|
|
235
|
+
if (dx < 0) {
|
|
236
|
+
next();
|
|
237
|
+
} else {
|
|
238
|
+
prev();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
start();
|
|
242
|
+
}
|
|
243
|
+
var dotsEl = slider.querySelector('.pin-slider-dots');
|
|
244
|
+
function onProgressEnd(e) {
|
|
245
|
+
if (e.animationName === 'pin-slider-progress') {
|
|
246
|
+
next();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (dotsEl) {
|
|
250
|
+
dotsEl.addEventListener('click', function(e) {
|
|
251
|
+
var btn = e.target.closest ? e.target.closest('.pin-slider-dot') : null;
|
|
252
|
+
if (btn) {
|
|
253
|
+
goTo(parseInt(btn.getAttribute('data-index'), 10) || 0);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
dotsEl.addEventListener('animationend', onProgressEnd);
|
|
257
|
+
}
|
|
258
|
+
var navPrev = slider.querySelector('.pin-slider-nav.prev');
|
|
259
|
+
var navNext = slider.querySelector('.pin-slider-nav.next');
|
|
260
|
+
if (navPrev) {
|
|
261
|
+
navPrev.addEventListener('click', prev);
|
|
262
|
+
}
|
|
263
|
+
if (navNext) {
|
|
264
|
+
navNext.addEventListener('click', next);
|
|
265
|
+
}
|
|
266
|
+
slider.addEventListener('mouseenter', stop);
|
|
267
|
+
slider.addEventListener('mouseleave', start);
|
|
268
|
+
slider.addEventListener('focusin', stop);
|
|
269
|
+
slider.addEventListener('focusout', start);
|
|
270
|
+
slider.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
271
|
+
slider.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
272
|
+
document.addEventListener('visibilitychange', onVis);
|
|
273
|
+
// 初始化直接定位到缓存序号:先禁用过渡再恢复,避免每次进页面播放“从第一张滑过来”的动画
|
|
274
|
+
track.style.transition = 'none';
|
|
275
|
+
goTo(index);
|
|
276
|
+
void track.offsetWidth;
|
|
277
|
+
track.style.transition = '';
|
|
278
|
+
slider.style.visibility = 'visible';
|
|
279
|
+
start();
|
|
280
|
+
return function() {
|
|
281
|
+
stop();
|
|
282
|
+
slider.removeEventListener('mouseenter', stop);
|
|
283
|
+
slider.removeEventListener('mouseleave', start);
|
|
284
|
+
slider.removeEventListener('focusin', stop);
|
|
285
|
+
slider.removeEventListener('focusout', start);
|
|
286
|
+
slider.removeEventListener('touchstart', onTouchStart);
|
|
287
|
+
slider.removeEventListener('touchend', onTouchEnd);
|
|
288
|
+
if (dotsEl) {
|
|
289
|
+
dotsEl.removeEventListener('animationend', onProgressEnd);
|
|
290
|
+
}
|
|
291
|
+
if (navPrev) {
|
|
292
|
+
navPrev.removeEventListener('click', prev);
|
|
293
|
+
}
|
|
294
|
+
if (navNext) {
|
|
295
|
+
navNext.removeEventListener('click', next);
|
|
296
|
+
}
|
|
297
|
+
document.removeEventListener('visibilitychange', onVis);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function restorePinIndex(group, contentKey, count) {
|
|
301
|
+
try {
|
|
302
|
+
var raw = localStorage.getItem('stellar.pin-slider.' + group);
|
|
303
|
+
if (!raw) return 0;
|
|
304
|
+
var data = JSON.parse(raw);
|
|
305
|
+
var sets = data && data.sets ? data.sets : {};
|
|
306
|
+
var rec = sets[contentKey];
|
|
307
|
+
if (rec && rec.t === count && rec.i >= 0 && rec.i < count) {
|
|
308
|
+
return rec.i;
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {}
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
function savePinIndex(group, contentKey, index, count) {
|
|
314
|
+
try {
|
|
315
|
+
var raw = localStorage.getItem('stellar.pin-slider.' + group);
|
|
316
|
+
var data = raw ? JSON.parse(raw) : {};
|
|
317
|
+
if (!data.sets) data.sets = {};
|
|
318
|
+
data.sets[contentKey] = { i: index, t: count };
|
|
319
|
+
localStorage.setItem('stellar.pin-slider.' + group, JSON.stringify(data));
|
|
320
|
+
} catch (e) {}
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
<%
|
|
324
|
+
}
|
|
325
|
+
%>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<% if ((is_home() || is_category() || is_tag()) && page.total > 1) { %>
|
|
2
2
|
<div class='paginator-wrap dis-select<%- scrollreveal(' ') %>'>
|
|
3
3
|
<%- paginator({
|
|
4
|
-
prev_text: '',
|
|
5
|
-
next_text: '',
|
|
6
|
-
force_prev_next:true
|
|
4
|
+
prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
|
|
5
|
+
next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
|
|
6
|
+
force_prev_next: true,
|
|
7
|
+
escape: false
|
|
7
8
|
}) %>
|
|
8
9
|
</div>
|
|
9
10
|
<% } %>
|
|
@@ -92,9 +92,7 @@ function div_default() {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
el += `<span class="pin">${icon('default:pin')}</span>`
|
|
97
|
-
}
|
|
95
|
+
// 置顶文章已由列表页 navbar 上方的置顶轮播展示,卡片内不再显示 pin 图标。
|
|
98
96
|
el += '</div>';
|
|
99
97
|
el += '</article>';
|
|
100
98
|
return el;
|
|
@@ -109,7 +107,7 @@ function div_photo() {
|
|
|
109
107
|
position = 'bottom';
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
|
-
el += '<div class="cover" position="' + position + '">';
|
|
110
|
+
el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + obj.image.replace(/'/g, '%27') + '\')">';
|
|
113
111
|
el += '<img src="' + obj.image + '"/>';
|
|
114
112
|
if (position.length > 0) {
|
|
115
113
|
el += '<div class="cover-info" position="' + position + '"';
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
const anchor = document.getElementById(anchorId);
|
|
33
33
|
if (anchor) {
|
|
34
|
-
|
|
34
|
+
// #start 锚点贴顶,不预留 offset;其余锚点与 TOC 点击滚动保持一致
|
|
35
|
+
const offset = anchorId === 'start' ? 0 : 32;
|
|
35
36
|
const targetY = () => anchor.getBoundingClientRect().top + window.scrollY - offset;
|
|
36
37
|
let timer = null;
|
|
37
38
|
let lastY = null;
|
|
@@ -26,6 +26,30 @@
|
|
|
26
26
|
window.lazyLoadInstance?.update();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
// 兜底:动态插入的 .lazy 元素(如站点自定义脚本在 update() 之后追加的图片)
|
|
30
|
+
// 不会被 vanilla-lazyload 自动扫描,未注册的 img.lazy 会被 CSS opacity:0 永久隐藏。
|
|
31
|
+
// MutationObserver 检测到新增 .lazy 元素后自动调用 update() 重新注册。
|
|
32
|
+
if (window.MutationObserver) {
|
|
33
|
+
window.lazyMutationObserver = new MutationObserver((mutations) => {
|
|
34
|
+
if (window.lazyMutationObserver._pending) return;
|
|
35
|
+
const hasLazy = mutations.some((m) =>
|
|
36
|
+
Array.prototype.some.call(m.addedNodes, (node) =>
|
|
37
|
+
node.nodeType === 1 &&
|
|
38
|
+
(node.matches?.('.lazy') || !!node.querySelector?.('.lazy'))
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
if (!hasLazy) return;
|
|
42
|
+
window.lazyMutationObserver._pending = requestAnimationFrame(() => {
|
|
43
|
+
window.lazyMutationObserver._pending = null;
|
|
44
|
+
window.lazyLoadInstance?.update();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
window.lazyMutationObserver.observe(document.documentElement, {
|
|
48
|
+
childList: true,
|
|
49
|
+
subtree: true
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
window.wrapLazyloadImages = (container) => {
|
|
30
54
|
if (typeof container === 'string') {
|
|
31
55
|
container = document.querySelector(container);
|
|
@@ -321,6 +321,31 @@
|
|
|
321
321
|
// 动态数据本地缓存:TTL 未过期直接命中;过期后先显示缓存再后台刷新(stale-while-revalidate)
|
|
322
322
|
_cacheEnabled: true,
|
|
323
323
|
_cacheMaxEntryBytes: 200 * 1024,
|
|
324
|
+
// 同 URL 并发请求去重:页面内多个组件请求同一接口时共享一次 fetch
|
|
325
|
+
_pendingRequests: {},
|
|
326
|
+
_fetchShared: (url, options) => {
|
|
327
|
+
const key = (options && options.method || 'GET') + ' ' + url;
|
|
328
|
+
if (utils._pendingRequests[key]) {
|
|
329
|
+
return utils._pendingRequests[key].then(resp => resp.clone());
|
|
330
|
+
}
|
|
331
|
+
const promise = fetch(url, options).then(resp => {
|
|
332
|
+
if (!resp.ok) throw new Error('响应失败');
|
|
333
|
+
return resp;
|
|
334
|
+
}).finally(() => {
|
|
335
|
+
delete utils._pendingRequests[key];
|
|
336
|
+
});
|
|
337
|
+
utils._pendingRequests[key] = promise;
|
|
338
|
+
// 每个调用方各自 clone,避免 Response body 被消费后其他调用方取不到数据
|
|
339
|
+
return promise.then(resp => resp.clone());
|
|
340
|
+
},
|
|
341
|
+
// 延迟执行:缓存写入等非关键操作让出主线程
|
|
342
|
+
_defer: (fn) => {
|
|
343
|
+
if (window.requestIdleCallback) {
|
|
344
|
+
window.requestIdleCallback(fn, { timeout: 3000 });
|
|
345
|
+
} else {
|
|
346
|
+
setTimeout(fn, 0);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
324
349
|
cache: {
|
|
325
350
|
prefix: 'Stellar.data_cache.v1.',
|
|
326
351
|
enabled: () => !!def.data_cache?.enable && utils._cacheEnabled,
|
|
@@ -500,21 +525,21 @@
|
|
|
500
525
|
}
|
|
501
526
|
}, 5000);
|
|
502
527
|
|
|
503
|
-
|
|
528
|
+
utils._fetchShared(url).then(resp => {
|
|
504
529
|
if (timedOut) return;
|
|
505
530
|
clearTimeout(timeout);
|
|
506
|
-
|
|
507
|
-
if (!resp.ok) throw new Error('响应失败');
|
|
508
531
|
return resp;
|
|
509
532
|
}).then(data => {
|
|
510
533
|
if (timedOut) return;
|
|
511
534
|
// 标记元素实例为已加载
|
|
512
535
|
if (el) utils._loadedElements.add(el);
|
|
513
|
-
//
|
|
536
|
+
// 写入缓存(克隆响应,避免影响回调读取;延迟执行,避免阻塞渲染)
|
|
514
537
|
if (cacheable && data.ok) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
538
|
+
utils._defer(() => {
|
|
539
|
+
data.clone().text().then(text => {
|
|
540
|
+
utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
|
|
541
|
+
}).catch(() => {});
|
|
542
|
+
});
|
|
518
543
|
}
|
|
519
544
|
// 后台刷新渲染前恢复初始结构,避免缓存渲染的内容重复
|
|
520
545
|
if (el && initialHTML !== null) {
|
|
@@ -562,30 +587,48 @@
|
|
|
562
587
|
|
|
563
588
|
return new Promise((resolve, reject) => {
|
|
564
589
|
let retryCount = 0;
|
|
590
|
+
// 与 request 保持一致:最终失败时回退 stale 缓存,避免闪失败态
|
|
591
|
+
const fallback = () => {
|
|
592
|
+
if (cached) {
|
|
593
|
+
resolve(utils.cache.toResponse(cached));
|
|
594
|
+
} else {
|
|
595
|
+
reject('timeout');
|
|
596
|
+
}
|
|
597
|
+
};
|
|
565
598
|
|
|
566
599
|
const tryRequest = () => {
|
|
567
600
|
let timedOut = false;
|
|
568
601
|
const timer = setTimeout(() => {
|
|
569
602
|
timedOut = true;
|
|
570
|
-
if (++retryCount > maxRetry)
|
|
603
|
+
if (++retryCount > maxRetry) fallback();
|
|
571
604
|
else tryRequest();
|
|
572
605
|
}, timeout);
|
|
573
606
|
|
|
574
|
-
|
|
607
|
+
utils._fetchShared(url, options)
|
|
575
608
|
.then(resp => {
|
|
576
609
|
clearTimeout(timer);
|
|
577
|
-
if (
|
|
610
|
+
if (timedOut) return;
|
|
578
611
|
if (cacheable) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
612
|
+
utils._defer(() => {
|
|
613
|
+
resp.clone().text().then(text => {
|
|
614
|
+
utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
|
|
615
|
+
}).catch(() => {});
|
|
616
|
+
});
|
|
582
617
|
}
|
|
583
618
|
resolve(resp);
|
|
584
619
|
})
|
|
585
620
|
.catch(err => {
|
|
586
621
|
clearTimeout(timer);
|
|
587
|
-
if (
|
|
588
|
-
|
|
622
|
+
if (timedOut) return;
|
|
623
|
+
if (++retryCount > maxRetry) {
|
|
624
|
+
if (cached) {
|
|
625
|
+
resolve(utils.cache.toResponse(cached));
|
|
626
|
+
} else {
|
|
627
|
+
reject(err);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
setTimeout(tryRequest, 500);
|
|
631
|
+
}
|
|
589
632
|
});
|
|
590
633
|
};
|
|
591
634
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%
|
|
2
|
+
// 隐藏的 SVG 线性渐变定义,配合 --item-grad 使用(用法见 menu.ejs 与 theme_base.styl 注释)。
|
|
3
|
+
// 渐变角度来自 style.gradient.angle(CSS 角度,0=向上、顺时针),与 CSS 渐变方向保持一致;
|
|
4
|
+
// 颜色使用 --item-theme(默认 var(--theme))与 --item-theme-light 两个 CSS 变量,
|
|
5
|
+
// 因此同一份定义可被任意元素复用,局部覆盖 --item-theme 的元素需同时设置 --item-theme-light。
|
|
6
|
+
var gradAngle = 225
|
|
7
|
+
if (theme.style && theme.style.gradient && theme.style.gradient.angle) {
|
|
8
|
+
gradAngle = parseFloat(theme.style.gradient.angle)
|
|
9
|
+
if (isNaN(gradAngle)) {
|
|
10
|
+
gradAngle = 225
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
var gradRad = gradAngle * Math.PI / 180
|
|
14
|
+
var gradVX = Math.sin(gradRad)
|
|
15
|
+
var gradVY = -Math.cos(gradRad)
|
|
16
|
+
var gradProj = [[0,0],[1,0],[0,1],[1,1]].map(function(c) {
|
|
17
|
+
return c[0] * gradVX + c[1] * gradVY
|
|
18
|
+
})
|
|
19
|
+
var gradMin = Math.min.apply(null, gradProj)
|
|
20
|
+
var gradMax = Math.max.apply(null, gradProj)
|
|
21
|
+
var gradX1 = Math.round((0.5 + gradVX * gradMin) * 1000) / 10
|
|
22
|
+
var gradY1 = Math.round((0.5 + gradVY * gradMin) * 1000) / 10
|
|
23
|
+
var gradX2 = Math.round((0.5 + gradVX * gradMax) * 1000) / 10
|
|
24
|
+
var gradY2 = Math.round((0.5 + gradVY * gradMax) * 1000) / 10
|
|
25
|
+
%>
|
|
26
|
+
<svg class="grad-def" width="0" height="0" aria-hidden="true" focusable="false"><defs><linearGradient id="<%= id %>" x1="<%= gradX1 %>%" y1="<%= gradY1 %>%" x2="<%= gradX2 %>%" y2="<%= gradY2 %>%"><stop offset="20%" style="stop-color:var(--item-theme-light)"/><stop offset="60%" style="stop-color:var(--item-theme)"/></linearGradient></defs></svg>
|