hexo-theme-fluid 1.9.8 → 1.9.9
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 +7 -18
- package/_config.yml +35 -9
- package/languages/de.yml +4 -0
- package/languages/en.yml +4 -0
- package/languages/eo.yml +4 -0
- package/languages/es.yml +4 -0
- package/languages/ja.yml +4 -0
- package/languages/ru.yml +4 -0
- package/languages/zh-CN.yml +4 -0
- package/languages/zh-HK.yml +4 -0
- package/languages/zh-TW.yml +11 -7
- package/layout/_partials/comments/cusdis.ejs +33 -1
- package/layout/_partials/comments/disqus.ejs +6 -1
- package/layout/_partials/comments/waline.ejs +3 -3
- package/layout/_partials/footer/statistics.ejs +17 -0
- package/layout/_partials/header/banner.ejs +39 -3
- package/layout/_partials/header/navigation.ejs +74 -1
- package/layout/_partials/plugins/analytics.ejs +5 -0
- package/layout/_partials/plugins/typed.ejs +4 -0
- package/layout/_partials/post/copyright.ejs +1 -1
- package/layout/_partials/post/meta-top.ejs +7 -1
- package/layout/_partials/post/toc.ejs +66 -2
- package/layout/index.ejs +1 -0
- package/package.json +1 -1
- package/scripts/events/index.js +1 -0
- package/scripts/events/lib/footnote.js +11 -2
- package/scripts/events/lib/random-banner.js +45 -0
- package/scripts/filters/post-filter.js +24 -1
- package/scripts/helpers/wordcount.js +3 -6
- package/scripts/tags/fold.js +1 -1
- package/scripts/tags/note.js +1 -1
- package/source/css/_pages/_base/_widget/header.styl +97 -0
- package/source/css/_pages/_base/_widget/toc.styl +32 -1
- package/source/css/_pages/_base/base.styl +3 -0
- package/source/css/_pages/_base/keyframes.styl +8 -0
- package/source/img/random/default.png +0 -0
- package/source/js/color-schema.js +33 -20
- package/source/js/events.js +46 -1
- package/source/js/openkounter.js +173 -0
- package/source/js/umami-view.js +6 -4
- package/source/js/utils.js +11 -6
package/package.json
CHANGED
package/scripts/events/index.js
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { stripHTML } = require('hexo-util');
|
|
4
4
|
|
|
5
|
+
function escapeAttr(text) {
|
|
6
|
+
return text
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
// Register footnotes filter
|
|
6
15
|
module.exports = (hexo) => {
|
|
7
16
|
const config = hexo.theme.config;
|
|
@@ -76,11 +85,11 @@ module.exports = (hexo) => {
|
|
|
76
85
|
if (!indexMap[index]) {
|
|
77
86
|
return match;
|
|
78
87
|
}
|
|
79
|
-
const tooltip = indexMap[index].content;
|
|
88
|
+
const tooltip = escapeAttr(stripHTML(indexMap[index].content));
|
|
80
89
|
return '<sup id="fnref:' + index + '" class="footnote-ref">'
|
|
81
90
|
+ '<a href="#fn:' + index + '" rel="footnote">'
|
|
82
91
|
+ '<span class="hint--top hint--rounded" aria-label="'
|
|
83
|
-
+
|
|
92
|
+
+ tooltip
|
|
84
93
|
+ '">[' + index + ']</span></a></sup>';
|
|
85
94
|
});
|
|
86
95
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const IMAGE_EXTS = new Set([
|
|
7
|
+
'.png',
|
|
8
|
+
'.jpg',
|
|
9
|
+
'.jpeg',
|
|
10
|
+
'.gif',
|
|
11
|
+
'.webp',
|
|
12
|
+
'.avif'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
module.exports = (hexo) => {
|
|
16
|
+
const themeConfig = hexo.theme && hexo.theme.config;
|
|
17
|
+
if (!themeConfig || !themeConfig.post) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const randomEnabled = themeConfig.banner && typeof themeConfig.banner.random_img === 'boolean'
|
|
22
|
+
? themeConfig.banner.random_img
|
|
23
|
+
: false;
|
|
24
|
+
if (!randomEnabled) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const randomDir = path.join(hexo.theme_dir, 'source', 'img', 'random');
|
|
29
|
+
if (!fs.existsSync(randomDir)) {
|
|
30
|
+
hexo.log.warn(`[Fluid] Random banner directory not found: ${randomDir}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const files = fs.readdirSync(randomDir)
|
|
35
|
+
.filter((file) => IMAGE_EXTS.has(path.extname(file).toLowerCase()));
|
|
36
|
+
|
|
37
|
+
if (files.length === 0) {
|
|
38
|
+
hexo.log.warn('[Fluid] Random banner directory is empty.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
themeConfig.banner_img_list = files.map((file) => `/img/random/${file}`);
|
|
43
|
+
hexo.log.debug(`[Fluid] Random banner images loaded: ${themeConfig.banner_img_list.length}`);
|
|
44
|
+
};
|
|
45
|
+
|
|
@@ -26,7 +26,7 @@ hexo.extend.filter.register('before_generate', function() {
|
|
|
26
26
|
});
|
|
27
27
|
const hidePosts = allPosts.filter(post => post.hide);
|
|
28
28
|
const normalPosts = allPosts.filter(post => !post.hide);
|
|
29
|
-
const indexPost = allPosts.filter(post => !post.hide && !post.archive)
|
|
29
|
+
const indexPost = allPosts.filter(post => !post.hide && !post.archive);
|
|
30
30
|
|
|
31
31
|
this.locals.set('all_posts', allPosts);
|
|
32
32
|
this.locals.set('hide_posts', hidePosts);
|
|
@@ -51,5 +51,28 @@ hexo.extend.filter.register('after_post_render', (page) => {
|
|
|
51
51
|
page.content = page.content.replace(/<colgroup>.+?<\/colgroup>/gims, '');
|
|
52
52
|
// 移除 hexo-renderer-pandoc 生成的 <span class="footnote-text">...<br>...</span>
|
|
53
53
|
page.content = page.content.replace(/(class="footnote-text".+?)<br.+?>(.+?rev="footnote")/gims, '$1$2');
|
|
54
|
+
|
|
55
|
+
// 为文章中 <h1> 添加默认 id 属性,方便锚点链接
|
|
56
|
+
if (page.content) {
|
|
57
|
+
const uniqueIdStore = {};
|
|
58
|
+
page.content = page.content.replace(/<h1>(.*?)<\/h1>/g, function(match, p1) {
|
|
59
|
+
const cleanId = p1.trim().toLowerCase()
|
|
60
|
+
.replace(/\s+/g, '-')
|
|
61
|
+
.replace(/[?#&]/g, '');
|
|
62
|
+
|
|
63
|
+
let uniqueId = cleanId;
|
|
64
|
+
if (cleanId === '') {
|
|
65
|
+
uniqueId = 'default';
|
|
66
|
+
}
|
|
67
|
+
if (uniqueIdStore[cleanId]) {
|
|
68
|
+
uniqueId = `${cleanId}-${uniqueIdStore[cleanId]}`;
|
|
69
|
+
uniqueIdStore[cleanId] += 1;
|
|
70
|
+
} else {
|
|
71
|
+
uniqueIdStore[cleanId] = 1;
|
|
72
|
+
}
|
|
73
|
+
return `<h1 id="${uniqueId}">${p1}</h1>`;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
return page;
|
|
55
78
|
});
|
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
const { stripHTML } = require('hexo-util');
|
|
6
6
|
|
|
7
7
|
const getWordCount = (post) => {
|
|
8
|
-
// post.origin is the original post content of hexo-blog-encrypt
|
|
9
|
-
const content = stripHTML(post.origin || post.content).replace(/\r?\n|\r/g, '').replace(/\s+/g, '');
|
|
10
|
-
|
|
11
8
|
if (!post.wordcount) {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
post.wordcount =
|
|
9
|
+
// post.origin is the original post content of hexo-blog-encrypt
|
|
10
|
+
const content = stripHTML(post.origin || post.content).replace(/[\s\r\n]/g, '');
|
|
11
|
+
post.wordcount = content.length;
|
|
15
12
|
}
|
|
16
13
|
return post.wordcount;
|
|
17
14
|
};
|
package/scripts/tags/fold.js
CHANGED
|
@@ -13,7 +13,7 @@ hexo.extend.tag.register('fold', (args, content) => {
|
|
|
13
13
|
</div>
|
|
14
14
|
<div class="fold-collapse collapse" id="${id}">
|
|
15
15
|
<div class="fold-content">
|
|
16
|
-
${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join('')}
|
|
16
|
+
${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join(' ')}
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
19
|
</div>`;
|
package/scripts/tags/note.js
CHANGED
|
@@ -7,7 +7,7 @@ const note = (args, content) => {
|
|
|
7
7
|
args = [ hexo.theme.config.post.updated.note_class || "info"];
|
|
8
8
|
}
|
|
9
9
|
return `<div class="note note-${args.join(' ')}">
|
|
10
|
-
${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join('')}
|
|
10
|
+
${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join(' ')}
|
|
11
11
|
</div>`;
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -172,3 +172,100 @@
|
|
|
172
172
|
|
|
173
173
|
#subtitle, .typed-cursor
|
|
174
174
|
font-size 1.35rem
|
|
175
|
+
|
|
176
|
+
// Mobile grid menu (九宫格)
|
|
177
|
+
#mobile-grid-menu
|
|
178
|
+
display block
|
|
179
|
+
position fixed
|
|
180
|
+
top 0
|
|
181
|
+
left 0
|
|
182
|
+
right 0
|
|
183
|
+
bottom 0
|
|
184
|
+
z-index 1029
|
|
185
|
+
overflow-y auto
|
|
186
|
+
-webkit-overflow-scrolling touch
|
|
187
|
+
pointer-events none
|
|
188
|
+
opacity 0
|
|
189
|
+
transform translateY(-12px)
|
|
190
|
+
transition opacity .18s ease, transform .18s ease
|
|
191
|
+
will-change opacity, transform
|
|
192
|
+
|
|
193
|
+
if $navbar-glass-enable
|
|
194
|
+
ground-glass($navbar-glass-px, $navbar-bg-color, $navbar-glass-alpha)
|
|
195
|
+
else
|
|
196
|
+
background-color var(--navbar-bg-color)
|
|
197
|
+
|
|
198
|
+
&.show
|
|
199
|
+
pointer-events auto
|
|
200
|
+
opacity 1
|
|
201
|
+
transform translateY(0)
|
|
202
|
+
|
|
203
|
+
// Staggered cell pop-in — delays are set via JS
|
|
204
|
+
.mobile-grid-cell, .mobile-grid-group-header
|
|
205
|
+
animation mobile-grid-cell-in .18s ease both
|
|
206
|
+
|
|
207
|
+
.mobile-grid-menu-inner
|
|
208
|
+
padding-top 70px
|
|
209
|
+
padding-bottom 2rem
|
|
210
|
+
|
|
211
|
+
.mobile-grid-group-header
|
|
212
|
+
color var(--navbar-text-color)
|
|
213
|
+
font-size 0.8rem
|
|
214
|
+
opacity 0.6
|
|
215
|
+
text-transform uppercase
|
|
216
|
+
letter-spacing 0.05em
|
|
217
|
+
padding 1rem 15px 0.4rem
|
|
218
|
+
border-top 1px solid rgba(255, 255, 255, 0.1)
|
|
219
|
+
margin-top 0.5rem
|
|
220
|
+
|
|
221
|
+
&:first-child
|
|
222
|
+
border-top none
|
|
223
|
+
margin-top 0
|
|
224
|
+
|
|
225
|
+
i
|
|
226
|
+
margin-right 0.3rem
|
|
227
|
+
font-size 0.8rem
|
|
228
|
+
|
|
229
|
+
.mobile-grid-cell
|
|
230
|
+
padding 0.5rem
|
|
231
|
+
|
|
232
|
+
.mobile-grid-item
|
|
233
|
+
display flex
|
|
234
|
+
flex-direction column
|
|
235
|
+
align-items center
|
|
236
|
+
justify-content center
|
|
237
|
+
padding 1rem 0.5rem
|
|
238
|
+
border-radius 12px
|
|
239
|
+
background-color rgba(255, 255, 255, 0.08)
|
|
240
|
+
transition background-color .2s ease-in-out, transform .15s ease-in-out
|
|
241
|
+
cursor pointer
|
|
242
|
+
min-height 80px
|
|
243
|
+
|
|
244
|
+
&:hover, &:active
|
|
245
|
+
background-color rgba(255, 255, 255, 0.18)
|
|
246
|
+
transform scale(0.96)
|
|
247
|
+
|
|
248
|
+
i
|
|
249
|
+
font-size 1.6rem
|
|
250
|
+
color var(--navbar-text-color)
|
|
251
|
+
margin-bottom 0.4rem
|
|
252
|
+
line-height 1
|
|
253
|
+
|
|
254
|
+
span
|
|
255
|
+
font-size 0.78rem
|
|
256
|
+
color var(--navbar-text-color)
|
|
257
|
+
text-align center
|
|
258
|
+
line-height 1.2
|
|
259
|
+
word-break break-word
|
|
260
|
+
|
|
261
|
+
a
|
|
262
|
+
text-decoration none
|
|
263
|
+
color var(--navbar-text-color)
|
|
264
|
+
|
|
265
|
+
&:hover
|
|
266
|
+
color var(--navbar-text-color)
|
|
267
|
+
|
|
268
|
+
// On mobile, suppress the original collapse list — grid menu takes over
|
|
269
|
+
@media (max-width: 991.98px)
|
|
270
|
+
#navbarSupportedContent
|
|
271
|
+
display none !important
|
|
@@ -32,9 +32,37 @@
|
|
|
32
32
|
a
|
|
33
33
|
font-size 0.95rem
|
|
34
34
|
|
|
35
|
+
ol a
|
|
36
|
+
font-size 0.85rem
|
|
37
|
+
|
|
35
38
|
.tocbot-link
|
|
36
39
|
color var(--text-color)
|
|
37
40
|
|
|
41
|
+
.toc-toggle
|
|
42
|
+
display inline-block
|
|
43
|
+
width .7rem
|
|
44
|
+
text-align center
|
|
45
|
+
margin-right .15rem
|
|
46
|
+
cursor pointer
|
|
47
|
+
user-select none
|
|
48
|
+
color var(--text-color)
|
|
49
|
+
font-size .9rem
|
|
50
|
+
line-height 1
|
|
51
|
+
transition transform .2s ease-in-out
|
|
52
|
+
|
|
53
|
+
&.toc-toggle-collapsed
|
|
54
|
+
transform rotate(0deg)
|
|
55
|
+
|
|
56
|
+
&.toc-toggle-expanded
|
|
57
|
+
transform rotate(90deg)
|
|
58
|
+
|
|
59
|
+
&.toc-toggle-placeholder
|
|
60
|
+
visibility hidden
|
|
61
|
+
pointer-events none
|
|
62
|
+
cursor default
|
|
63
|
+
transform none
|
|
64
|
+
|
|
65
|
+
|
|
38
66
|
.tocbot-active-link
|
|
39
67
|
font-weight bold
|
|
40
68
|
color var(--link-hover-color)
|
|
@@ -47,7 +75,10 @@
|
|
|
47
75
|
transition all .3s ease-in-out
|
|
48
76
|
|
|
49
77
|
.toc-list-item
|
|
50
|
-
|
|
78
|
+
padding .1rem 0
|
|
79
|
+
display -webkit-box
|
|
80
|
+
-webkit-box-orient vertical
|
|
81
|
+
-webkit-line-clamp 2
|
|
51
82
|
overflow hidden
|
|
52
83
|
text-overflow ellipsis
|
|
53
84
|
|
|
Binary file
|
|
@@ -153,33 +153,38 @@
|
|
|
153
153
|
}
|
|
154
154
|
var iconElement = document.querySelector(colorToggleIconSelector);
|
|
155
155
|
if (iconElement) {
|
|
156
|
-
iconElement.setAttribute(
|
|
157
|
-
|
|
158
|
-
'iconfont ' + icon
|
|
159
|
-
);
|
|
160
|
-
iconElement.setAttribute(
|
|
161
|
-
'data',
|
|
162
|
-
invertColorSchemaObj[schema]
|
|
163
|
-
);
|
|
156
|
+
iconElement.setAttribute('class', 'iconfont ' + icon);
|
|
157
|
+
iconElement.setAttribute('data', invertColorSchemaObj[schema]);
|
|
164
158
|
} else {
|
|
165
|
-
// 如果图标不存在则说明图标还没加载出来,等到页面全部加载再尝试切换
|
|
166
159
|
Fluid.utils.waitElementLoaded(colorToggleIconSelector, function() {
|
|
167
160
|
var iconElement = document.querySelector(colorToggleIconSelector);
|
|
168
161
|
if (iconElement) {
|
|
169
|
-
iconElement.setAttribute(
|
|
170
|
-
|
|
171
|
-
'iconfont ' + icon
|
|
172
|
-
);
|
|
173
|
-
iconElement.setAttribute(
|
|
174
|
-
'data',
|
|
175
|
-
invertColorSchemaObj[schema]
|
|
176
|
-
);
|
|
162
|
+
iconElement.setAttribute('class', 'iconfont ' + icon);
|
|
163
|
+
iconElement.setAttribute('data', invertColorSchemaObj[schema]);
|
|
177
164
|
}
|
|
178
165
|
});
|
|
179
166
|
}
|
|
167
|
+
|
|
168
|
+
// 同步更新移动端按钮文字:light 状态显示"关灯",dark 状态显示"开灯"
|
|
169
|
+
var mobileBtn = document.querySelector('#mobile-color-toggle-btn');
|
|
170
|
+
if (mobileBtn) {
|
|
171
|
+
var label = document.querySelector('#mobile-color-toggle-label');
|
|
172
|
+
if (label) {
|
|
173
|
+
// schema 是当前模式:light→显示 dark 标签(关灯),dark→显示 light 标签(开灯)
|
|
174
|
+
label.textContent = schema === 'dark'
|
|
175
|
+
? (mobileBtn.getAttribute('data-label-light') || '')
|
|
176
|
+
: (mobileBtn.getAttribute('data-label-dark') || '');
|
|
177
|
+
}
|
|
178
|
+
// 同步图标
|
|
179
|
+
var mobileIcon = document.querySelector('#mobile-color-toggle-icon');
|
|
180
|
+
if (mobileIcon) {
|
|
181
|
+
mobileIcon.setAttribute('class', 'iconfont ' + icon);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
180
185
|
if (document.documentElement.getAttribute('data-user-color-scheme')) {
|
|
181
|
-
var color = getComputedStyle(document.documentElement).getPropertyValue('--navbar-bg-color').trim()
|
|
182
|
-
document.querySelector('meta[name="theme-color"]').setAttribute('content', color)
|
|
186
|
+
var color = getComputedStyle(document.documentElement).getPropertyValue('--navbar-bg-color').trim();
|
|
187
|
+
document.querySelector('meta[name="theme-color"]').setAttribute('content', color);
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
}
|
|
@@ -277,10 +282,18 @@
|
|
|
277
282
|
});
|
|
278
283
|
}
|
|
279
284
|
}
|
|
285
|
+
|
|
286
|
+
// 绑定移动端菜单按钮的事件
|
|
287
|
+
var mobileButton = document.querySelector('#mobile-color-toggle-btn');
|
|
288
|
+
if (mobileButton) {
|
|
289
|
+
mobileButton.addEventListener('click', function() {
|
|
290
|
+
applyCustomColorSchemaSettings(toggleCustomColorSchema());
|
|
291
|
+
});
|
|
292
|
+
}
|
|
280
293
|
});
|
|
281
294
|
|
|
282
295
|
Fluid.utils.waitElementLoaded(iframeSelector, function() {
|
|
283
296
|
applyCustomColorSchemaSettings();
|
|
284
297
|
});
|
|
285
|
-
|
|
298
|
+
|
|
286
299
|
})(window, document);
|
package/source/js/events.js
CHANGED
|
@@ -29,9 +29,54 @@ Fluid.events = {
|
|
|
29
29
|
submenu.removeClass('navbar-dark');
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
var mobileGridMenu = jQuery('#mobile-grid-menu');
|
|
34
|
+
|
|
32
35
|
jQuery('#navbar-toggler-btn').on('click', function() {
|
|
36
|
+
var $this = jQuery(this);
|
|
37
|
+
if ($this.data('animating')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
$this.data('animating', true);
|
|
33
41
|
jQuery('.animated-icon').toggleClass('open');
|
|
34
|
-
|
|
42
|
+
|
|
43
|
+
// On mobile use grid menu; on desktop keep original collapse behavior
|
|
44
|
+
if (window.innerWidth < 992) {
|
|
45
|
+
navbar.addClass('top-nav-collapse');
|
|
46
|
+
mobileGridMenu.toggleClass('show');
|
|
47
|
+
// Apply staggered animation delays when opening
|
|
48
|
+
if (mobileGridMenu.hasClass('show')) {
|
|
49
|
+
mobileGridMenu.find('.mobile-grid-cell, .mobile-grid-group-header').each(function(i, el) {
|
|
50
|
+
el.style.animationDelay = (i * 20) + 'ms';
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Prevent body scroll when menu is open
|
|
54
|
+
jQuery('body').toggleClass('mobile-menu-open', mobileGridMenu.hasClass('show'));
|
|
55
|
+
} else {
|
|
56
|
+
jQuery('#navbar').toggleClass('navbar-col-show');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setTimeout(function() {
|
|
60
|
+
$this.data('animating', false);
|
|
61
|
+
}, 300);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Close grid menu when a link inside it is clicked
|
|
65
|
+
mobileGridMenu.on('click', 'a[href]:not([href="javascript:;"])', function() {
|
|
66
|
+
mobileGridMenu.removeClass('show');
|
|
67
|
+
jQuery('.animated-icon').removeClass('open');
|
|
68
|
+
jQuery('body').removeClass('mobile-menu-open');
|
|
69
|
+
navbar.removeClass('top-nav-collapse');
|
|
70
|
+
jQuery('#navbar-toggler-btn').data('animating', false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Close grid menu on resize to desktop
|
|
74
|
+
jQuery(window).on('resize', function() {
|
|
75
|
+
if (window.innerWidth >= 992 && mobileGridMenu.hasClass('show')) {
|
|
76
|
+
mobileGridMenu.removeClass('show');
|
|
77
|
+
jQuery('.animated-icon').removeClass('open');
|
|
78
|
+
jQuery('body').removeClass('mobile-menu-open');
|
|
79
|
+
}
|
|
35
80
|
});
|
|
36
81
|
},
|
|
37
82
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* global CONFIG, Fluid */
|
|
2
|
+
|
|
3
|
+
(function(window, document) {
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
// Get server URL from config
|
|
7
|
+
const API_SERVER = (CONFIG.web_analytics.openkounter && CONFIG.web_analytics.openkounter.server_url) || '';
|
|
8
|
+
|
|
9
|
+
if (!API_SERVER) {
|
|
10
|
+
console.warn('OpenKounter: server_url is not configured');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getRecord(target) {
|
|
15
|
+
return fetch(`${API_SERVER}/api/counter?target=${encodeURIComponent(target)}`)
|
|
16
|
+
.then(resp => {
|
|
17
|
+
if (!resp.ok) {
|
|
18
|
+
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
19
|
+
}
|
|
20
|
+
return resp.json();
|
|
21
|
+
})
|
|
22
|
+
.then(({ data, code, message }) => {
|
|
23
|
+
if (code !== 0) {
|
|
24
|
+
throw new Error(message || 'Unknown error');
|
|
25
|
+
}
|
|
26
|
+
return { time: data.time || 0, objectId: data.target };
|
|
27
|
+
})
|
|
28
|
+
.catch(error => {
|
|
29
|
+
console.error('OpenKounter fetch error:', error);
|
|
30
|
+
return { time: 0, objectId: target };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function increment(incrArr) {
|
|
35
|
+
if (!incrArr || incrArr.length === 0) {
|
|
36
|
+
return Promise.resolve([]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fetch(`${API_SERVER}/api/counter`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
action: 'batch_inc',
|
|
44
|
+
requests: incrArr
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
.then(res => {
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
return res.json();
|
|
52
|
+
})
|
|
53
|
+
.then(res => {
|
|
54
|
+
if (res.code !== 0) {
|
|
55
|
+
throw new Error(res.message || 'Failed to increment counter');
|
|
56
|
+
}
|
|
57
|
+
return res.data;
|
|
58
|
+
})
|
|
59
|
+
.catch(error => {
|
|
60
|
+
console.error('OpenKounter increment error:', error);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildIncrement(objectId) {
|
|
65
|
+
return { target: objectId };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 校验是否为有效的主机(排除本地开发环境)
|
|
69
|
+
function validHost() {
|
|
70
|
+
const ignoreLocal = CONFIG.web_analytics.openkounter && CONFIG.web_analytics.openkounter.ignore_local;
|
|
71
|
+
if (ignoreLocal !== false) {
|
|
72
|
+
const hostname = window.location.hostname;
|
|
73
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 校验是否为有效的独立访客(24小时内只计一次)
|
|
81
|
+
function validUV() {
|
|
82
|
+
const key = 'OpenKounter_UV_Flag';
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const flag = localStorage.getItem(key);
|
|
87
|
+
if (flag) {
|
|
88
|
+
const lastVisit = parseInt(flag, 10);
|
|
89
|
+
// 距离上次访问小于 24 小时则不计为新 UV
|
|
90
|
+
if (now - lastVisit <= 86400000) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
localStorage.setItem(key, now.toString());
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// localStorage 不可用时默认计为 UV
|
|
97
|
+
console.warn('OpenKounter: localStorage is not available');
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addCount() {
|
|
103
|
+
const enableIncr = CONFIG.web_analytics.enable && (!window.Fluid || !Fluid.ctx.dnt) && validHost();
|
|
104
|
+
const getterArr = [];
|
|
105
|
+
const incrArr = [];
|
|
106
|
+
|
|
107
|
+
// 请求站点 PV 并自增
|
|
108
|
+
const pvCtn = document.querySelector('#openkounter-site-pv-container');
|
|
109
|
+
if (pvCtn) {
|
|
110
|
+
const pvGetter = getRecord('site-pv').then((record) => {
|
|
111
|
+
if (enableIncr) {
|
|
112
|
+
incrArr.push(buildIncrement(record.objectId));
|
|
113
|
+
}
|
|
114
|
+
const ele = document.querySelector('#openkounter-site-pv');
|
|
115
|
+
if (ele) {
|
|
116
|
+
ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0);
|
|
117
|
+
pvCtn.style.display = 'inline';
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
getterArr.push(pvGetter);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 请求站点 UV 并自增
|
|
124
|
+
const uvCtn = document.querySelector('#openkounter-site-uv-container');
|
|
125
|
+
if (uvCtn) {
|
|
126
|
+
const uvGetter = getRecord('site-uv').then((record) => {
|
|
127
|
+
const incrUV = validUV() && enableIncr;
|
|
128
|
+
if (incrUV) {
|
|
129
|
+
incrArr.push(buildIncrement(record.objectId));
|
|
130
|
+
}
|
|
131
|
+
const ele = document.querySelector('#openkounter-site-uv');
|
|
132
|
+
if (ele) {
|
|
133
|
+
ele.innerText = (record.time || 0) + (incrUV ? 1 : 0);
|
|
134
|
+
uvCtn.style.display = 'inline';
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
getterArr.push(uvGetter);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 请求页面浏览数并自增
|
|
141
|
+
const viewCtn = document.querySelector('#openkounter-page-views-container');
|
|
142
|
+
if (viewCtn) {
|
|
143
|
+
const pathConfig = CONFIG.web_analytics.openkounter.path || 'window.location.pathname';
|
|
144
|
+
const path = eval(pathConfig);
|
|
145
|
+
const target = decodeURI(path.replace(/\/*(index.html)?$/, '/'));
|
|
146
|
+
|
|
147
|
+
const viewGetter = getRecord(target).then((record) => {
|
|
148
|
+
if (enableIncr) {
|
|
149
|
+
incrArr.push(buildIncrement(record.objectId));
|
|
150
|
+
}
|
|
151
|
+
const ele = document.querySelector('#openkounter-page-views');
|
|
152
|
+
if (ele) {
|
|
153
|
+
ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0);
|
|
154
|
+
viewCtn.style.display = 'inline';
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
getterArr.push(viewGetter);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 批量发起统计请求
|
|
161
|
+
Promise.all(getterArr).then(() => {
|
|
162
|
+
if (enableIncr && incrArr.length > 0) {
|
|
163
|
+
increment(incrArr);
|
|
164
|
+
}
|
|
165
|
+
}).catch(error => {
|
|
166
|
+
console.error('OpenKounter error:', error);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
addCount();
|
|
171
|
+
|
|
172
|
+
})(window, document);
|
|
173
|
+
|
package/source/js/umami-view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 从配置文件中获取 umami 的配置
|
|
2
2
|
const website_id = CONFIG.web_analytics.umami.website_id;
|
|
3
3
|
// 拼接请求地址
|
|
4
|
-
const request_url = `${CONFIG.web_analytics.umami.api_server}/websites/${website_id}/stats`;
|
|
4
|
+
const request_url = `${CONFIG.web_analytics.umami.api_server}/api/websites/${website_id}/stats`;
|
|
5
5
|
|
|
6
6
|
const start_time = new Date(CONFIG.web_analytics.umami.start_time).getTime();
|
|
7
7
|
const end_time = new Date().getTime();
|
|
@@ -31,7 +31,7 @@ const request_header = {
|
|
|
31
31
|
method: "GET",
|
|
32
32
|
headers: {
|
|
33
33
|
"Content-Type": "application/json",
|
|
34
|
-
"
|
|
34
|
+
"Authorization": "Bearer " + token,
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ async function siteStats() {
|
|
|
40
40
|
try {
|
|
41
41
|
const response = await fetch(`${request_url}?${params}`, request_header);
|
|
42
42
|
const data = await response.json();
|
|
43
|
-
const uniqueVisitors = data.
|
|
43
|
+
const uniqueVisitors = data.visitors.value; // 获取独立访客数
|
|
44
44
|
const pageViews = data.pageviews.value; // 获取页面浏览量
|
|
45
45
|
|
|
46
46
|
let pvCtn = document.querySelector("#umami-site-pv-container");
|
|
@@ -94,6 +94,8 @@ let viewCtn = document.querySelector("#umami-page-views-container");
|
|
|
94
94
|
// 如果页面容器存在,则获取页面浏览量
|
|
95
95
|
if (viewCtn) {
|
|
96
96
|
let path = window.location.pathname;
|
|
97
|
-
let target =
|
|
97
|
+
let target = path
|
|
98
|
+
.replace(/(\/[^/]+\.html)\/$/, "$1") // 如果是 '/xxxx.html/' 格式的路径,则去掉最后那个 '/'
|
|
99
|
+
.replace(/\/index\.html$/, "/"); // 如果是 '/index.html' 格式,则合并成 '/'
|
|
98
100
|
pageStats(target);
|
|
99
101
|
}
|