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.
Files changed (41) hide show
  1. package/README.md +7 -18
  2. package/_config.yml +35 -9
  3. package/languages/de.yml +4 -0
  4. package/languages/en.yml +4 -0
  5. package/languages/eo.yml +4 -0
  6. package/languages/es.yml +4 -0
  7. package/languages/ja.yml +4 -0
  8. package/languages/ru.yml +4 -0
  9. package/languages/zh-CN.yml +4 -0
  10. package/languages/zh-HK.yml +4 -0
  11. package/languages/zh-TW.yml +11 -7
  12. package/layout/_partials/comments/cusdis.ejs +33 -1
  13. package/layout/_partials/comments/disqus.ejs +6 -1
  14. package/layout/_partials/comments/waline.ejs +3 -3
  15. package/layout/_partials/footer/statistics.ejs +17 -0
  16. package/layout/_partials/header/banner.ejs +39 -3
  17. package/layout/_partials/header/navigation.ejs +74 -1
  18. package/layout/_partials/plugins/analytics.ejs +5 -0
  19. package/layout/_partials/plugins/typed.ejs +4 -0
  20. package/layout/_partials/post/copyright.ejs +1 -1
  21. package/layout/_partials/post/meta-top.ejs +7 -1
  22. package/layout/_partials/post/toc.ejs +66 -2
  23. package/layout/index.ejs +1 -0
  24. package/package.json +1 -1
  25. package/scripts/events/index.js +1 -0
  26. package/scripts/events/lib/footnote.js +11 -2
  27. package/scripts/events/lib/random-banner.js +45 -0
  28. package/scripts/filters/post-filter.js +24 -1
  29. package/scripts/helpers/wordcount.js +3 -6
  30. package/scripts/tags/fold.js +1 -1
  31. package/scripts/tags/note.js +1 -1
  32. package/source/css/_pages/_base/_widget/header.styl +97 -0
  33. package/source/css/_pages/_base/_widget/toc.styl +32 -1
  34. package/source/css/_pages/_base/base.styl +3 -0
  35. package/source/css/_pages/_base/keyframes.styl +8 -0
  36. package/source/img/random/default.png +0 -0
  37. package/source/js/color-schema.js +33 -20
  38. package/source/js/events.js +46 -1
  39. package/source/js/openkounter.js +173 -0
  40. package/source/js/umami-view.js +6 -4
  41. package/source/js/utils.js +11 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-fluid",
3
- "version": "1.9.8",
3
+ "version": "1.9.9",
4
4
  "description": "An elegant Material-Design theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "files": [
@@ -4,6 +4,7 @@
4
4
 
5
5
  hexo.on('generateBefore', () => {
6
6
  require('./lib/merge-configs')(hexo);
7
+ require('./lib/random-banner')(hexo);
7
8
  require('./lib/compatible-configs')(hexo);
8
9
  require('./lib/injects')(hexo);
9
10
  require('./lib/highlight')(hexo);
@@ -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, '&lt;')
11
+ .replace(/>/g, '&gt;');
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
- + stripHTML(tooltip)
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
- const zhCount = (content.match(/[\u4E00-\u9FA5]/g) || []).length;
13
- const enCount = (content.replace(/[\u4E00-\u9FA5]/g, '').match(/[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g) || []).length;
14
- post.wordcount = zhCount + enCount
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
  };
@@ -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>`;
@@ -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
- white-space nowrap
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
 
@@ -66,3 +66,6 @@ label
66
66
  i.iconfont
67
67
  font-size 1em
68
68
  line-height 1
69
+
70
+ body.mobile-menu-open
71
+ overflow hidden
@@ -29,3 +29,11 @@
29
29
  100%
30
30
  -webkit-transform translateY(0)
31
31
  transform translateY(0)
32
+
33
+ @keyframes mobile-grid-cell-in
34
+ from
35
+ opacity 0
36
+ transform scale(0.82) translateY(10px)
37
+ to
38
+ opacity 1
39
+ transform scale(1) translateY(0)
Binary file
@@ -153,33 +153,38 @@
153
153
  }
154
154
  var iconElement = document.querySelector(colorToggleIconSelector);
155
155
  if (iconElement) {
156
- iconElement.setAttribute(
157
- 'class',
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
- 'class',
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);
@@ -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
- jQuery('#navbar').toggleClass('navbar-col-show');
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
+
@@ -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
- "x-umami-api-key": "oZKCH3msvqt10VlXKwoJvHclmaS4bVx0",
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.uniques.value; // 获取独立访客数
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 = decodeURI(path.replace(/\/*(index.html)?$/, "/"));
97
+ let target = path
98
+ .replace(/(\/[^/]+\.html)\/$/, "$1") // 如果是 '/xxxx.html/' 格式的路径,则去掉最后那个 '/'
99
+ .replace(/\/index\.html$/, "/"); // 如果是 '/index.html' 格式,则合并成 '/'
98
100
  pageStats(target);
99
101
  }