hexo-theme-stellar 1.32.3 → 1.33.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.
Files changed (93) hide show
  1. package/_config.yml +27 -7
  2. package/_data/icons.yml +9 -2
  3. package/languages/en.yml +1 -0
  4. package/languages/zh-CN.yml +1 -0
  5. package/languages/zh-TW.yml +1 -0
  6. package/layout/_partial/comments/artalk/script.ejs +8 -7
  7. package/layout/_partial/head.ejs +7 -5
  8. package/layout/_partial/main/article/article_footer.ejs +8 -0
  9. package/layout/_partial/main/article/contributors.ejs +34 -0
  10. package/layout/_partial/main/article/read_next.ejs +2 -2
  11. package/layout/_partial/main/navbar/article_banner.ejs +1 -1
  12. package/layout/_partial/main/navbar/breadcrumb/blog.ejs +3 -3
  13. package/layout/_partial/main/navbar/breadcrumb/note.ejs +2 -2
  14. package/layout/_partial/main/navbar/breadcrumb/page.ejs +1 -1
  15. package/layout/_partial/main/navbar/breadcrumb/wiki.ejs +2 -2
  16. package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
  17. package/layout/_partial/main/navbar/ghinfo.ejs +1 -1
  18. package/layout/_partial/main/navbar/nav_tabs_blog.ejs +12 -12
  19. package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +2 -2
  20. package/layout/_partial/main/notebook/note_tags.ejs +1 -1
  21. package/layout/_partial/scripts/lazyload.ejs +1 -1
  22. package/layout/_partial/scripts/utils.ejs +69 -37
  23. package/layout/_partial/sidebar/index_leftbar.ejs +1 -1
  24. package/layout/_partial/sidebar/logo.ejs +1 -1
  25. package/layout/_partial/sidebar/menu.ejs +1 -1
  26. package/layout/_partial/widgets/components/link.ejs +1 -1
  27. package/layout/_partial/widgets/ghissues.ejs +7 -1
  28. package/layout/_partial/widgets/ghrepo.ejs +2 -2
  29. package/layout/_partial/widgets/ghuser.ejs +1 -1
  30. package/layout/_partial/widgets/recent.ejs +1 -1
  31. package/layout/_partial/widgets/related.ejs +2 -2
  32. package/layout/_partial/widgets/tagtree.ejs +1 -1
  33. package/layout/_partial/widgets/timeline.ejs +7 -1
  34. package/layout/_partial/widgets/toc.ejs +1 -3
  35. package/layout/_partial/widgets/tree.ejs +1 -1
  36. package/layout/archive.ejs +1 -1
  37. package/layout/categories.ejs +1 -1
  38. package/layout/index.ejs +1 -1
  39. package/layout/index_topic.ejs +1 -1
  40. package/layout/index_wiki.ejs +2 -2
  41. package/layout/notebooks.ejs +1 -1
  42. package/layout/notes.ejs +1 -1
  43. package/layout/tags.ejs +1 -1
  44. package/package.json +1 -1
  45. package/scripts/events/index.js +1 -1
  46. package/scripts/events/lib/config.js +7 -1
  47. package/scripts/filters/pretty_urls.js +25 -0
  48. package/scripts/generators/notebooks.js +27 -26
  49. package/scripts/helpers/json_ld.js +5 -4
  50. package/scripts/helpers/pretty_url.js +22 -0
  51. package/scripts/tags/index.js +2 -0
  52. package/scripts/tags/lib/albums.js +1 -1
  53. package/scripts/tags/lib/chat.js +1 -1
  54. package/scripts/tags/lib/friends.js +1 -1
  55. package/scripts/tags/lib/link.js +1 -1
  56. package/scripts/tags/lib/posters.js +1 -1
  57. package/scripts/tags/lib/rating.js +32 -0
  58. package/scripts/tags/lib/sites.js +1 -1
  59. package/scripts/tags/lib/timeline.js +2 -1
  60. package/scripts/tags/lib/vote.js +28 -0
  61. package/source/css/_common/button.styl +1 -0
  62. package/source/css/_common/loading.styl +1 -0
  63. package/source/css/_components/pages/article-story.styl +2 -0
  64. package/source/css/_components/partial/article-footer.styl +48 -0
  65. package/source/css/_components/partial/paginator.styl +1 -1
  66. package/source/css/_components/tag-plugins/friends.styl +1 -1
  67. package/source/css/_components/tag-plugins/image.styl +1 -0
  68. package/source/css/_components/tag-plugins/rating.styl +39 -0
  69. package/source/css/_components/tag-plugins/timeline.styl +7 -4
  70. package/source/css/_components/tag-plugins/vote.styl +49 -0
  71. package/source/css/_components/widgets/timeline.styl +1 -1
  72. package/source/css/_defines/theme_base.styl +1 -0
  73. package/source/css/_plugins/comments/artalk.styl +53 -10
  74. package/source/css/_plugins/lazyload.styl +4 -0
  75. package/source/js/main.js +12 -5
  76. package/source/js/search/local-search.js +3 -1
  77. package/source/js/services/artalk_latest_comment.js +1 -1
  78. package/source/js/services/contributors.js +56 -0
  79. package/source/js/services/fcircle.js +1 -1
  80. package/source/js/services/friends.js +1 -1
  81. package/source/js/services/friends_and_posts.js +1 -1
  82. package/source/js/services/ghinfo.js +1 -1
  83. package/source/js/services/giscus_latest_comment.js +1 -1
  84. package/source/js/services/memos.js +1 -1
  85. package/source/js/services/rating.js +142 -0
  86. package/source/js/services/siteinfo.js +1 -1
  87. package/source/js/services/sites.js +1 -1
  88. package/source/js/services/timeline.js +1 -1
  89. package/source/js/services/twikoo_latest_comment.js +1 -1
  90. package/source/js/services/vote.js +113 -0
  91. package/source/js/services/waline_latest_comment.js +1 -1
  92. package/source/js/services/weibo.js +1 -1
  93. package/layout/_partial/widgets/components/edit.ejs +0 -24
@@ -6,7 +6,7 @@
6
6
  font-size: 1rem
7
7
  font-weight: 700
8
8
  background: var(--card)
9
- border-radius: $border-card
9
+ border-radius: $border-card-l
10
10
  overflow: hidden
11
11
  box-shadow: 0 2px 8px 0px rgba(0, 0, 0, 0.03)
12
12
  color: var(--text-p3)
@@ -13,7 +13,7 @@
13
13
  align-items: center
14
14
  text-align: center
15
15
  line-height: 1.2
16
- border-radius: 4px
16
+ border-radius: $border-button
17
17
  overflow: hidden
18
18
  position: relative
19
19
  padding: 1rem 0.5rem
@@ -22,6 +22,7 @@
22
22
  img
23
23
  display: block
24
24
  object-fit: cover
25
+ border-radius: $border-image
25
26
  .image-download
26
27
  position: absolute
27
28
  bottom: 8px
@@ -0,0 +1,39 @@
1
+ .tag-plugin.ds-rating
2
+ text-align: center
3
+ .tag-plugin.ds-rating .header
4
+ margin: .5rem
5
+ font-weight: 500
6
+ color: var(--text)
7
+ .tag-plugin.ds-rating .body
8
+ display: flex
9
+ align-items: center
10
+ justify-content: center
11
+ button
12
+ color: var(--text-p1)
13
+ font-size: 1rem
14
+ padding: 0
15
+ border-radius: $border-button
16
+ display: flex
17
+ svg
18
+ font-size: 1.5em
19
+ margin: 4px
20
+ img
21
+ margin: 4px
22
+ trans1 all
23
+ color: var(--theme)
24
+ opacity: 0.2
25
+ &:hover,&.hover,&.preview
26
+ opacity: 1
27
+ span
28
+ line-height: 1
29
+ font-size: 1em
30
+ margin: 4px
31
+
32
+ .tag-plugin.ds-rating .footer
33
+ margin: .5rem
34
+ color: var(--text-p3)
35
+ font-weight: 500
36
+ font-size: $fs-12
37
+
38
+ .tag-plugin.ds-rating.rated
39
+ pointer-events: none
@@ -102,7 +102,7 @@
102
102
  .tag-plugin.copy
103
103
  width: 240px
104
104
 
105
- .tag-plugin.timeline[api]
105
+ .tag-plugin.timeline[data-api]
106
106
  a.body
107
107
  display: block
108
108
  color: var(--text-p1)
@@ -112,7 +112,7 @@
112
112
  font-size: var(--fsp)
113
113
 
114
114
 
115
- .tag-plugin.timeline[api] .body
115
+ .tag-plugin.timeline[data-api] .body
116
116
  p.title
117
117
  font-weight: 700
118
118
  margin: 0.5rem 0 0.75rem
@@ -149,7 +149,10 @@
149
149
  pre code
150
150
  font-size: $fs-13
151
151
 
152
- .tag-plugin.timeline[api] .body .footer
152
+ aside .tag-plugin.timeline[data-api] .body .lazy-box
153
+ border-radius: $border-image-s
154
+
155
+ .tag-plugin.timeline[data-api] .body .footer
153
156
  margin: 0 0 -0.5rem
154
157
  padding: 0.5rem 0 1rem
155
158
  user-select: none
@@ -170,7 +173,7 @@
170
173
  .left+.right
171
174
  margin-left: 4px
172
175
 
173
- .tag-plugin.timeline[api] .body .footer
176
+ .tag-plugin.timeline[data-api] .body .footer
174
177
  .item
175
178
  border-width: 1px
176
179
  border-style: solid
@@ -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
@@ -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
 
@@ -8,6 +8,8 @@
8
8
  border-bottom: 1px dashed var(--block-border)
9
9
  padding: 0.5rem 1rem
10
10
  margin: 0
11
+ .atk-editor-user
12
+ padding: 0
11
13
  .atk-editor-user-wrap:empty
12
14
  visibility: hidden
13
15
  padding: 0
@@ -67,6 +69,10 @@
67
69
  height: 3rem
68
70
  width: 12rem
69
71
  margin: -1.5rem auto 0
72
+ &:hover
73
+ background: var(--card)
74
+ .atk-list-footer .atk-copyright
75
+ padding: 1rem
70
76
  .atk-error-layer
71
77
  background: none
72
78
 
@@ -95,16 +101,52 @@
95
101
  --at-color-green: $c-green
96
102
  --at-color-gradient: linear-gradient(180deg, transparent, var(--card), 50%, var(--card))
97
103
 
98
- .md-text .atk-comment>.atk-main>.atk-body
99
- overflow: visible
100
- .md-text .artalk pre code, .atk-layer-wrap pre code
101
- background: none
102
- .md-text .atk-main-editor>.atk-textarea-wrap>.atk-textarea
103
- background: none
104
104
 
105
- .md-text .atk-notify-wrap .atk-notify
106
- border-radius: $border-bar
107
- 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
108
150
 
109
151
 
110
152
  // override
@@ -118,11 +160,12 @@
118
160
  border-radius: $border-image-s
119
161
  a>img
120
162
  display: inline
121
- p>img[atk-emoticon]
163
+ img[atk-emoticon]
122
164
  max-height: 1.5em
123
165
  transform: scale(1.2)
124
166
  margin: 0 4px
125
167
  vertical-align: bottom
168
+ border-radius: 4px
126
169
  ul li, ol li
127
170
  font-size: $fs-14
128
171
  blockquote
@@ -1,9 +1,13 @@
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
10
+ line-height: 0
7
11
  &.async // 通过 data-service 异步创建的懒加载元素,没办法知道宽高
8
12
  background: var(--block)
9
13
  min-height: 4rem
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
  }
@@ -0,0 +1,56 @@
1
+ utils.jq(() => {
2
+ $(function () {
3
+ function parseGithubFileContributors(data) {
4
+ // 去重贡献者(按 login)
5
+ const contributorsMap = new Map();
6
+
7
+ for (const commit of data) {
8
+ const author = commit.author;
9
+ if (author) {
10
+ const login = author.login;
11
+ if (!contributorsMap.has(login)) {
12
+ contributorsMap.set(login, {
13
+ login: login,
14
+ avatar_url: author.avatar_url,
15
+ html_url: author.html_url,
16
+ count: 1
17
+ });
18
+ } else {
19
+ contributorsMap.get(login).count++;
20
+ }
21
+ }
22
+ }
23
+ // 转为数组并按提交次数排序(降序)
24
+ const sortedContributors = Array.from(contributorsMap.values())
25
+ .sort((a, b) => b.count - a.count);
26
+ return sortedContributors;
27
+ }
28
+
29
+ const els = document.getElementsByClassName('ds-contributors');
30
+ for (var i = 0; i < els.length; i++) {
31
+ const el = els[i];
32
+ const api = el.dataset.api;
33
+ if (api == null) {
34
+ continue;
35
+ }
36
+ const default_avatar = def.avatar;
37
+ // layout
38
+ utils.request(el, api, async resp => {
39
+ const data = await resp.json();
40
+ const list = parseGithubFileContributors(data);
41
+ for (let item of list) {
42
+ var cell = `<div class="grid-cell user-card">`;
43
+ cell += `<a class="card-link" target="_blank" rel="external nofollow noopener noreferrer" href="${item.html_url || item.url}">`;;
44
+ cell += `<img src="${item.avatar_url || item.avatar || item.icon || default_avatar}" onerror="javascript:this.removeAttribute(\'data-src\');this.src=\'${default_avatar}\';"/>`;
45
+ cell += `<div class="name image-meta">`;
46
+ cell += `<span class="image-caption">${item.title || item.login}</span>`;
47
+ cell += `</div>`;
48
+ cell += `</a>`;
49
+ cell += `</div>`;
50
+ $(el).find('.grid-box').append(cell);
51
+ }
52
+ window.wrapLazyloadImages(el);
53
+ });
54
+ }
55
+ });
56
+ });
@@ -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;