hexo-theme-stellar 1.32.4 → 1.33.1

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 (89) hide show
  1. package/_config.yml +24 -14
  2. package/_data/icons.yml +8 -1
  3. package/languages/en.yml +1 -1
  4. package/languages/zh-CN.yml +1 -1
  5. package/languages/zh-TW.yml +1 -1
  6. package/layout/_partial/comments/artalk/script.ejs +7 -6
  7. package/layout/_partial/head.ejs +7 -1
  8. package/layout/_partial/main/article/contributors.ejs +1 -1
  9. package/layout/_partial/main/article/read_next.ejs +2 -2
  10. package/layout/_partial/main/navbar/article_banner.ejs +1 -1
  11. package/layout/_partial/main/navbar/breadcrumb/blog.ejs +3 -3
  12. package/layout/_partial/main/navbar/breadcrumb/note.ejs +2 -2
  13. package/layout/_partial/main/navbar/breadcrumb/page.ejs +1 -1
  14. package/layout/_partial/main/navbar/breadcrumb/wiki.ejs +2 -2
  15. package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
  16. package/layout/_partial/main/navbar/ghinfo.ejs +1 -1
  17. package/layout/_partial/main/navbar/nav_tabs_blog.ejs +12 -12
  18. package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +2 -2
  19. package/layout/_partial/main/notebook/note_tags.ejs +1 -1
  20. package/layout/_partial/scripts/lazyload.ejs +1 -1
  21. package/layout/_partial/scripts/utils.ejs +69 -39
  22. package/layout/_partial/sidebar/index_leftbar.ejs +1 -1
  23. package/layout/_partial/sidebar/menu.ejs +1 -1
  24. package/layout/_partial/widgets/components/link.ejs +1 -1
  25. package/layout/_partial/widgets/ghissues.ejs +7 -1
  26. package/layout/_partial/widgets/ghrepo.ejs +2 -2
  27. package/layout/_partial/widgets/ghuser.ejs +1 -1
  28. package/layout/_partial/widgets/recent.ejs +1 -1
  29. package/layout/_partial/widgets/related.ejs +2 -2
  30. package/layout/_partial/widgets/tagtree.ejs +1 -1
  31. package/layout/_partial/widgets/timeline.ejs +7 -1
  32. package/layout/_partial/widgets/tree.ejs +1 -1
  33. package/layout/_plugins/mathjax.ejs +47 -18
  34. package/layout/archive.ejs +1 -1
  35. package/layout/categories.ejs +1 -1
  36. package/layout/index.ejs +1 -1
  37. package/layout/index_topic.ejs +1 -1
  38. package/layout/index_wiki.ejs +2 -2
  39. package/layout/notebooks.ejs +1 -1
  40. package/layout/notes.ejs +1 -1
  41. package/layout/tags.ejs +1 -1
  42. package/package.json +1 -1
  43. package/scripts/events/index.js +1 -1
  44. package/scripts/events/lib/config.js +7 -1
  45. package/scripts/filters/pretty_urls.js +25 -0
  46. package/scripts/helpers/json_ld.js +5 -4
  47. package/scripts/helpers/pretty_url.js +22 -0
  48. package/scripts/tags/index.js +2 -0
  49. package/scripts/tags/lib/albums.js +1 -1
  50. package/scripts/tags/lib/chat.js +1 -1
  51. package/scripts/tags/lib/friends.js +1 -1
  52. package/scripts/tags/lib/link.js +1 -1
  53. package/scripts/tags/lib/posters.js +1 -1
  54. package/scripts/tags/lib/rating.js +32 -0
  55. package/scripts/tags/lib/sites.js +1 -1
  56. package/scripts/tags/lib/timeline.js +2 -1
  57. package/scripts/tags/lib/vote.js +28 -0
  58. package/source/css/_common/button.styl +1 -0
  59. package/source/css/_components/md.styl +2 -0
  60. package/source/css/_components/pages/article-story.styl +2 -0
  61. package/source/css/_components/partial/navbar.styl +2 -2
  62. package/source/css/_components/tag-plugins/image.styl +1 -0
  63. package/source/css/_components/tag-plugins/rating.styl +39 -0
  64. package/source/css/_components/tag-plugins/timeline.styl +5 -5
  65. package/source/css/_components/tag-plugins/vote.styl +49 -0
  66. package/source/css/_components/widgets/timeline.styl +1 -1
  67. package/source/css/_custom.styl +4 -2
  68. package/source/css/_defines/func.styl +16 -8
  69. package/source/css/_defines/theme_base.styl +2 -1
  70. package/source/css/_plugins/comments/artalk.styl +51 -11
  71. package/source/css/_plugins/lazyload.styl +3 -0
  72. package/source/js/main.js +12 -5
  73. package/source/js/search/local-search.js +3 -1
  74. package/source/js/services/artalk_latest_comment.js +1 -1
  75. package/source/js/services/contributors.js +1 -1
  76. package/source/js/services/fcircle.js +1 -1
  77. package/source/js/services/friends.js +1 -1
  78. package/source/js/services/friends_and_posts.js +1 -1
  79. package/source/js/services/ghinfo.js +1 -1
  80. package/source/js/services/giscus_latest_comment.js +1 -1
  81. package/source/js/services/memos.js +1 -1
  82. package/source/js/services/rating.js +142 -0
  83. package/source/js/services/siteinfo.js +1 -1
  84. package/source/js/services/sites.js +1 -1
  85. package/source/js/services/timeline.js +1 -1
  86. package/source/js/services/twikoo_latest_comment.js +1 -1
  87. package/source/js/services/vote.js +113 -0
  88. package/source/js/services/waline_latest_comment.js +1 -1
  89. package/source/js/services/weibo.js +1 -1
@@ -0,0 +1,49 @@
1
+ .tag-plugin.ds-vote
2
+ text-align: center
3
+ .tag-plugin.ds-vote .header
4
+ margin: .5rem
5
+ font-weight: 500
6
+ color: var(--text)
7
+ .tag-plugin.ds-vote .body
8
+ width 50%
9
+ min-width: 240px
10
+ max-width: 320px
11
+ margin: 1rem auto
12
+ display: grid
13
+ grid-template-columns: 1fr 1fr
14
+ grid-gap: 2rem
15
+ button
16
+ color: var(--text-p1)
17
+ font-size: 1rem
18
+ display: inline-flex
19
+ flex-direction: column
20
+ align-items: center
21
+ justify-content: center
22
+ padding: .5rem
23
+ background: var(--block)
24
+ border-radius: $border-button
25
+ font-weight: 500
26
+ svg
27
+ font-size: 1.5em
28
+ margin: 8px
29
+ span
30
+ line-height: 1
31
+ font-size: 1em
32
+ margin-bottom: 4px
33
+ trans1 all
34
+ color: var(--theme)
35
+ background: var(--theme-a10)
36
+ @media (prefers-color-scheme: dark)
37
+ background: var(--theme-a20)
38
+ &.vote-up
39
+ x-set-theme-with-color($c-green)
40
+ &.vote-down
41
+ x-set-theme-with-color($c-red)
42
+ &:hover,&.active
43
+ color: white
44
+ background: var(--theme)
45
+
46
+
47
+ // 已经投过票了
48
+ .tag-plugin.ds-vote.voted
49
+ pointer-events: none
@@ -36,7 +36,7 @@
36
36
  background: var(--bg-a50)
37
37
 
38
38
 
39
- .tag-plugin.timeline[api] .body .footer
39
+ .tag-plugin.timeline[data-api] .body .footer
40
40
  background: none
41
41
 
42
42
  .l_left .widget-wrapper.timeline
@@ -35,8 +35,10 @@ $leftbar-background-color = hexo-config('style.leftbar.background-color')
35
35
 
36
36
 
37
37
  // colors
38
- $c-theme = hsl(192deg, 98%, 55%)
39
- $c-accent = hsl(14deg, 100%, 57%)
38
+ $c-theme = convert(hexo-config('style.color.theme'))
39
+ $c-accent = convert(hexo-config('style.color.accent'))
40
+ $c-link = convert(hexo-config('style.color.link'))
41
+
40
42
  $c-base-hue = 210deg // 背景/文本颜色的主基调(一般看不出来不需要修改)
41
43
 
42
44
  // icons
@@ -136,6 +136,13 @@ hoverable-card()
136
136
  newblur()
137
137
  box-shadow: 0 0 2px rgba(black, .04), 0 2px 8px rgba(black, .04), 0 4px 16px rgba(black, .04)
138
138
  trans1: all
139
+ text-shadow: 0 0 4px rgba(white, 1), 0 0 8px rgba(white, 0.8), 0 0 16px rgba(white, 0.6)
140
+ @media (prefers-color-scheme: dark)
141
+ text-shadow: 0 0 2px rgba(black, 0.2), 0 0 4px rgba(black, 0.4)
142
+ nav a:not(.active,:hover)
143
+ filter: drop-shadow(0 0 0 white) drop-shadow(0 0 4px white)
144
+ @media (prefers-color-scheme: dark)
145
+ filter: none
139
146
  &:before,&:after
140
147
  z-index -1
141
148
  position: absolute
@@ -149,17 +156,18 @@ newblur()
149
156
  &:before
150
157
  backdrop-filter: blur(8px)
151
158
  -webkit-backdrop-filter: blur(8px)
152
- background: var(--bg-a20)
159
+ background: rgba(white, 0.3)
160
+ margin: 1px
161
+ @media (prefers-color-scheme: dark)
162
+ background: rgba(white, 0.2)
153
163
  &:after
154
- --blur-px: 24px
155
- --blur-sat: 500%
156
- background: var(--bg-a50)
164
+ --blur-px: 16px
165
+ --blur-sat: 300%
157
166
  backdrop-filter: saturate(var(--blur-sat)) blur(var(--blur-px))
158
167
  -webkit-backdrop-filter: saturate(var(--blur-sat)) blur(var(--blur-px))
159
- mask: linear-gradient(rgba(white, 1), 10%, rgba(white, 0.05), 90%, rgba(white, 1)), linear-gradient(90deg, rgba(white, 1), 10%, rgba(white, 0.05), 90%, rgba(white, 1))
160
- -webkit-mask: linear-gradient(rgba(white, 1), 10%, rgba(white, 0.05), 90%, rgba(white, 1)), linear-gradient(90deg, rgba(white, 1), 10%, rgba(white, 0.05), 90%, rgba(white, 1))
168
+ mask: linear-gradient(white, 20%, transparent, 80%, white), linear-gradient(90deg, white, 20%, transparent, 80%, white)
169
+ -webkit-mask-image: linear-gradient(white, 20%, transparent, 80%, white), linear-gradient(90deg, white, 20%, transparent, 80%, white)
161
170
  box-shadow: inset 0 0 4px 1px rgba(white, 0.5)
162
171
  trans1 all
163
172
  @media (prefers-color-scheme: dark)
164
- opacity 0.4
165
- background: var(--bg-a20)
173
+ box-shadow: inset 0 0 2px 1px rgba(white, 0.2)
@@ -13,6 +13,7 @@ x-set-theme-with-color($color)
13
13
  --light: lightness($color)
14
14
  --alpha: alpha($color)
15
15
  --theme: x-theme-alpha(1)
16
+ --theme-a10: x-theme-alpha(0.1)
16
17
  --theme-a20: x-theme-alpha(0.2)
17
18
  --theme-a30: x-theme-alpha(0.3)
18
19
 
@@ -90,7 +91,7 @@ dynamic-theme-dark()
90
91
  // 强调色
91
92
  --accent: $c-accent
92
93
  // 链接
93
- x-set-link-with-color(hsl(207deg, 90%, 54%))
94
+ x-set-link-with-color($c-link)
94
95
 
95
96
  // dynamic colors
96
97
  dynamic-theme-light()
@@ -69,6 +69,10 @@
69
69
  height: 3rem
70
70
  width: 12rem
71
71
  margin: -1.5rem auto 0
72
+ &:hover
73
+ background: var(--card)
74
+ .atk-list-footer .atk-copyright
75
+ padding: 1rem
72
76
  .atk-error-layer
73
77
  background: none
74
78
 
@@ -97,17 +101,52 @@
97
101
  --at-color-green: $c-green
98
102
  --at-color-gradient: linear-gradient(180deg, transparent, var(--card), 50%, var(--card))
99
103
 
100
- .md-text .atk-comment>.atk-main>.atk-body
101
- overflow: visible
102
- .md-text .artalk pre code, .atk-layer-wrap pre code
103
- background: none
104
- .md-text .atk-main-editor>.atk-textarea-wrap>.atk-textarea
105
- background: none
106
- padding: 0.5rem 1rem
107
104
 
108
- .md-text .atk-notify-wrap .atk-notify
109
- border-radius: $border-bar
110
- margin: 8px
105
+ // fix style
106
+ .cmt-body.artalk
107
+ .atk-comment>.atk-main>.atk-body
108
+ overflow: visible
109
+ .artalk pre code, .atk-layer-wrap pre code
110
+ background: none
111
+ .atk-main-editor>.atk-textarea-wrap>.atk-textarea
112
+ background: none
113
+ padding: 1rem
114
+ margin: 0
115
+ border: none
116
+
117
+ .atk-loading
118
+ border-radius: $border-card
119
+ .atk-main-editor .atk-editor-user-wrap .atk-editor-user .atk-user-profile-btn
120
+ padding: 0
121
+ &:hover
122
+ background: none
123
+
124
+ .artalk>.atk-list>.atk-list-header
125
+ padding: .5rem 1rem
126
+ .atk-comment-count
127
+ display: flex
128
+ align-items: baseline
129
+ .artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-arrow-down-icon
130
+ height: 8px
131
+ .artalk>.atk-list>.atk-list-header .atk-text
132
+ display: block
133
+
134
+ .atk-notify-wrap .atk-notify
135
+ border-radius: $border-bar
136
+ margin: 8px
137
+
138
+ // emoticons
139
+ .atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp
140
+ padding: .5rem
141
+ margin-bottom: 32px
142
+ .atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp[data-type=image]>.atk-item
143
+ width 3rem
144
+ height: 3rem
145
+ margin: 0
146
+ border-radius: 4px
147
+ padding: .5rem
148
+ >img
149
+ border-radius: 4px
111
150
 
112
151
 
113
152
  // override
@@ -121,11 +160,12 @@
121
160
  border-radius: $border-image-s
122
161
  a>img
123
162
  display: inline
124
- p>img[atk-emoticon]
163
+ img[atk-emoticon]
125
164
  max-height: 1.5em
126
165
  transform: scale(1.2)
127
166
  margin: 0 4px
128
167
  vertical-align: bottom
168
+ border-radius: 4px
129
169
  ul li, ol li
130
170
  font-size: $fs-14
131
171
  blockquote
@@ -1,6 +1,9 @@
1
1
  $lazyTransitionType = hexo-config('plugins.lazyload.transition')
2
2
  $loadingImageSize = 2rem
3
3
 
4
+ img:not([src])
5
+ visibility: hidden
6
+
4
7
  .lazy-box
5
8
  position: relative
6
9
  overflow: hidden
package/source/js/main.js CHANGED
@@ -229,20 +229,27 @@ const init = {
229
229
  document.body.appendChild(notice);
230
230
  }
231
231
  if (!canonical.originalHost) return;
232
+ const currentURL = new URL(window.location.href);
233
+ const currentHost = currentURL.hostname.replace(/^www\./, '');
234
+ if (currentHost == 'localhost') return;
235
+ const encodedCurrentHost = window.btoa(currentHost);
236
+ const isCurrentHostValid = canonical.encoded === encodedCurrentHost;
232
237
  const canonicalTag = document.querySelector('link[rel="canonical"]');
233
238
  if (!canonicalTag) {
239
+ if (isCurrentHostValid) {
240
+ return;
241
+ }
242
+ if (canonical.officialHosts?.includes(currentHost)) {
243
+ showTip(true);
244
+ return;
245
+ }
234
246
  showTip(false);
235
247
  return;
236
248
  }
237
249
  const canonicalURL = new URL(canonicalTag.href);
238
- const currentURL = new URL(window.location.href);
239
250
  const canonicalHost = canonicalURL.hostname.replace(/^www\./, '');
240
- const currentHost = currentURL.hostname.replace(/^www\./, '');
241
- if (currentHost == 'localhost') return;
242
251
  const encodedCanonicalHost = window.btoa(canonicalHost);
243
- const encodedCurrentHost = window.btoa(currentHost);
244
252
  const isCanonicalHostValid = canonical.encoded === encodedCanonicalHost;
245
- const isCurrentHostValid = canonical.encoded === encodedCurrentHost;
246
253
  if (isCanonicalHostValid && isCurrentHostValid) {
247
254
  return;
248
255
  }
@@ -48,7 +48,9 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
48
48
  var dataContent = data.content;
49
49
  var dataContentLowerCase = dataContent.toLowerCase();
50
50
  var dataUrl = data.path.startsWith('//') ? data.path.slice(1) : data.path;
51
-
51
+ dataUrl = dataUrl.replace(/\/?index\.html$/, '/'); // index.html → /
52
+ dataUrl = dataUrl.replace(/\.html$/, '/'); // xxx.html → xxx/
53
+
52
54
  var indexTitle = -1;
53
55
  var indexContent = -1;
54
56
  var firstOccur = -1;
@@ -5,7 +5,7 @@ utils.jq(() => {
5
5
  const el = els[i];
6
6
  const limit = parseInt(el.getAttribute('limit')) || 10;
7
7
 
8
- const api = el.getAttribute('api') + '&limit=' + limit;
8
+ const api = el.dataset.api + '&limit=' + limit;
9
9
  if (api == null) {
10
10
  continue;
11
11
  }
@@ -29,7 +29,7 @@ utils.jq(() => {
29
29
  const els = document.getElementsByClassName('ds-contributors');
30
30
  for (var i = 0; i < els.length; i++) {
31
31
  const el = els[i];
32
- const api = el.getAttribute('api');
32
+ const api = el.dataset.api;
33
33
  if (api == null) {
34
34
  continue;
35
35
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-fcircle');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-friends');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-friends_and_posts');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-ghinfo');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-giscus');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -2,7 +2,7 @@ utils.jq(() => {
2
2
  const els = Array.from(document.getElementsByClassName('ds-memos'));
3
3
 
4
4
  els.forEach(el => {
5
- const api = el.getAttribute('api');
5
+ const api = el.dataset.api;
6
6
  if (!api) return;
7
7
 
8
8
  const default_avatar = el.getAttribute('avatar') || def.avatar;
@@ -0,0 +1,142 @@
1
+ function getRatingKey(id) {
2
+ return `rating-${id}`;
3
+ }
4
+
5
+ function hasRated(id) {
6
+ return !!localStorage.getItem(getRatingKey(id));
7
+ }
8
+
9
+ function getRatedValue(id) {
10
+ return parseInt(localStorage.getItem(getRatingKey(id)) || '0');
11
+ }
12
+
13
+ function storeRating(id, value) {
14
+ localStorage.setItem(getRatingKey(id), value);
15
+ }
16
+
17
+ function clearHover(el) {
18
+ el.querySelectorAll('.star').forEach(s => s.classList.remove('hover'));
19
+ }
20
+
21
+ function updatePreview(el, avg) {
22
+ const rounded = Math.floor(avg);
23
+ el.querySelectorAll('.star').forEach(s => {
24
+ const v = parseInt(s.dataset.value);
25
+ s.classList.toggle('preview', v <= rounded);
26
+ });
27
+ }
28
+
29
+ function setupHoverEffect(el) {
30
+ const stars = el.querySelectorAll('.star');
31
+ if (!stars.length) return;
32
+
33
+ stars.forEach(star => {
34
+ const value = parseInt(star.dataset.value);
35
+
36
+ star.addEventListener('mouseenter', () => {
37
+ stars.forEach(s => {
38
+ s.classList.remove('preview');
39
+ const v = parseInt(s.dataset.value);
40
+ s.classList.toggle('hover', v <= value);
41
+ });
42
+ });
43
+
44
+ star.addEventListener('mouseleave', () => {
45
+ clearHover(el);
46
+ // 恢复平均分预览
47
+ const avg = parseFloat(el.querySelector('.avg')?.textContent.replace(/[()]/g, '') || '0');
48
+ updatePreview(el, avg);
49
+ });
50
+ });
51
+ }
52
+
53
+ function calculateAverage(rating = {}) {
54
+ const validScores = Object.entries(rating).filter(([k]) => !isNaN(Number(k)));
55
+ const total = validScores.reduce((sum, [k, c]) => sum + Number(k) * c, 0);
56
+ const votes = validScores.reduce((sum, [, c]) => sum + c, 0);
57
+ return votes > 0 ? (total / votes).toFixed(1) : '0.0';
58
+ }
59
+
60
+ async function loadRating(el) {
61
+ const id = el.dataset.id;
62
+ const api = el.dataset.api;
63
+ if (!id || !api) return;
64
+
65
+ try {
66
+ const res = await fetch(`${api}/info?id=${encodeURIComponent(id)}`);
67
+ const data = await res.json();
68
+ const rating = data.rating || {};
69
+ const avg = calculateAverage(rating);
70
+
71
+ // 计算评分人数
72
+ const validScores = Object.entries(rating).filter(([k]) => !isNaN(Number(k)));
73
+ const totalVotes = validScores.reduce((sum, [, c]) => sum + c, 0);
74
+
75
+ // 设置平均分
76
+ let avgEl = el.querySelector('.avg');
77
+ if (!avgEl) {
78
+ avgEl = document.createElement('span');
79
+ avgEl.className = 'avg';
80
+ el.appendChild(avgEl);
81
+ }
82
+ avgEl.textContent = `(${avg})`;
83
+
84
+ // 设置评分人数
85
+ let countEl = el.querySelector('.count');
86
+ if (!countEl) {
87
+ countEl = document.createElement('span');
88
+ countEl.className = 'count';
89
+ el.appendChild(countEl);
90
+ }
91
+ countEl.textContent = `${totalVotes}`;
92
+
93
+ updatePreview(el, avg);
94
+ } catch (e) {
95
+ console.warn(`[rating] 加载失败: id=${id}`, e);
96
+ }
97
+ }
98
+
99
+ async function submitRating(el, value) {
100
+ const id = el.dataset.id;
101
+ const api = el.dataset.api;
102
+ if (!id || !api || hasRated(id)) return;
103
+
104
+ storeRating(id, value);
105
+ el.classList.add('rated');
106
+
107
+ try {
108
+ await fetch(`${api}/update?id=${encodeURIComponent(id)}&value=${value}`, {
109
+ method: 'POST'
110
+ });
111
+ loadRating(el);
112
+ } catch (e) {
113
+ console.warn(`[rating] 提交失败: id=${id}`, e);
114
+ }
115
+ }
116
+
117
+ function initRatings() {
118
+ document.querySelectorAll('.ds-rating').forEach(el => {
119
+ const { id, api } = el.dataset;
120
+ if (!id || !api) return;
121
+
122
+ loadRating(el);
123
+ setupHoverEffect(el);
124
+
125
+ if (hasRated(id)) {
126
+ el.classList.add('rated');
127
+ }
128
+
129
+ el.querySelectorAll('.star').forEach(star => {
130
+ const value = star.dataset.value;
131
+ star.addEventListener('click', () => {
132
+ if (!hasRated(id)) submitRating(el, value);
133
+ });
134
+ });
135
+ });
136
+ }
137
+
138
+ if (document.readyState === 'loading') {
139
+ window.addEventListener('DOMContentLoaded', initRatings);
140
+ } else {
141
+ initRatings();
142
+ }
@@ -6,7 +6,7 @@ function setCardLink(nodes) {
6
6
  // If it is not a tag element then it is not processed
7
7
  if (el.nodeType !== 1) return;
8
8
  el.removeAttribute('cardlink');
9
- const api = el.getAttribute('api');
9
+ const api = el.dataset.api;
10
10
  if (api == null) return;
11
11
  fetch(api).then(function(response) {
12
12
  if (response.ok) {
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-sites');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -13,7 +13,7 @@ utils.jq(() => {
13
13
  const timelines = document.getElementsByClassName('ds-timeline');
14
14
  for (var i = 0; i < timelines.length; i++) {
15
15
  const el = timelines[i];
16
- const api = el.getAttribute('api');
16
+ const api = el.dataset.api;
17
17
  if (api == null) {
18
18
  continue;
19
19
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const el = document.querySelector('.ds-twikoo');
4
4
  utils.onLoading(el); // 加载动画
5
5
 
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  const limit = parseInt(el.getAttribute('limit')) || 10;
8
8
  const reply = el.getAttribute('hide') !== 'reply';
9
9
  if (!api) return;
@@ -0,0 +1,113 @@
1
+ function getVoteKey(id) {
2
+ return `vote-${id}`;
3
+ }
4
+
5
+ function hasVoted(id) {
6
+ return !!localStorage.getItem(getVoteKey(id));
7
+ }
8
+
9
+ function getVotedValue(id) {
10
+ return localStorage.getItem(getVoteKey(id)); // 'up' 或 'down'
11
+ }
12
+
13
+ function storeVote(id, value) {
14
+ localStorage.setItem(getVoteKey(id), value);
15
+ }
16
+
17
+ function removeVote(id) {
18
+ localStorage.removeItem(getVoteKey(id));
19
+ }
20
+
21
+ function markVoted(el, value) {
22
+ el.classList.add('voted');
23
+ if (value === 'up') {
24
+ el.querySelector('.vote-up')?.classList.add('active');
25
+ } else if (value === 'down') {
26
+ el.querySelector('.vote-down')?.classList.add('active');
27
+ }
28
+ }
29
+
30
+ function revertVote(el, value) {
31
+ const id = el.dataset.id;
32
+
33
+ // 回退数值
34
+ if (value === 'up') {
35
+ const upEl = el.querySelector('.up');
36
+ if (upEl) upEl.textContent = Math.max(0, parseInt(upEl.textContent || '1') - 1);
37
+ } else if (value === 'down') {
38
+ const downEl = el.querySelector('.down');
39
+ if (downEl) downEl.textContent = Math.max(0, parseInt(downEl.textContent || '1') - 1);
40
+ }
41
+
42
+ // 回退样式和状态
43
+ el.classList.remove('voted', 'active');
44
+ el.querySelector('.vote-up')?.classList.remove('active');
45
+ el.querySelector('.vote-down')?.classList.remove('active');
46
+ removeVote(id);
47
+ }
48
+
49
+ async function loadVote(el) {
50
+ const id = el.dataset.id;
51
+ const api = el.dataset.api;
52
+ if (!id || !api) return;
53
+
54
+ try {
55
+ const res = await fetch(`${api}/info?id=${encodeURIComponent(id)}`);
56
+ const data = await res.json();
57
+
58
+ el.querySelector('.up').textContent = data.votes?.up ?? 0;
59
+ el.querySelector('.down').textContent = data.votes?.down ?? 0;
60
+ } catch (e) {
61
+ console.warn(`[vote] 加载失败: id=${id}`, e);
62
+ }
63
+ }
64
+
65
+ function submitVote(el, value) {
66
+ const id = el.dataset.id;
67
+ const api = el.dataset.api;
68
+ if (!id || !api || hasVoted(id)) return;
69
+
70
+ const upEl = el.querySelector('.up');
71
+ const downEl = el.querySelector('.down');
72
+
73
+ // 乐观更新
74
+ if (value === 'up' && upEl) upEl.textContent = parseInt(upEl.textContent || '0') + 1;
75
+ if (value === 'down' && downEl) downEl.textContent = parseInt(downEl.textContent || '0') + 1;
76
+
77
+ storeVote(id, value);
78
+ markVoted(el, value);
79
+
80
+ // 后台同步
81
+ fetch(`${api}/update?id=${encodeURIComponent(id)}&value=${encodeURIComponent(value)}`, {
82
+ method: 'POST'
83
+ }).catch(e => {
84
+ console.warn(`[vote] 后台同步失败,撤销投票: id=${id}`, e);
85
+ revertVote(el, value);
86
+ });
87
+ }
88
+
89
+ function initVotes() {
90
+ document.querySelectorAll('.ds-vote').forEach(el => {
91
+ const { id, api } = el.dataset;
92
+ if (!id || !api) return;
93
+
94
+ loadVote(el);
95
+
96
+ const votedValue = getVotedValue(id);
97
+ if (votedValue) markVoted(el, votedValue);
98
+
99
+ el.querySelector('.vote-up')?.addEventListener('click', () => {
100
+ if (!el.classList.contains('active')) submitVote(el, 'up');
101
+ });
102
+
103
+ el.querySelector('.vote-down')?.addEventListener('click', () => {
104
+ if (!el.classList.contains('active')) submitVote(el, 'down');
105
+ });
106
+ });
107
+ }
108
+
109
+ if (document.readyState === 'loading') {
110
+ window.addEventListener('DOMContentLoaded', initVotes);
111
+ } else {
112
+ initVotes();
113
+ }
@@ -4,7 +4,7 @@ utils.jq(() => {
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
6
  const limit = parseInt(el.getAttribute('limit')) || 10;
7
- const apiBase = el.getAttribute('api');
7
+ const apiBase = el.dataset.api;
8
8
  if (apiBase == null) {
9
9
  continue;
10
10
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-weibo');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }