hexo-theme-stellar 1.39.0 → 1.40.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 (54) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +2 -2
  2. package/CHANGELOG.md +44 -0
  3. package/_config.yml +8 -2
  4. package/languages/en.yml +2 -0
  5. package/languages/zh-CN.yml +2 -0
  6. package/languages/zh-TW.yml +2 -0
  7. package/layout/_partial/comments/artalk/script.ejs +8 -0
  8. package/layout/_partial/cover/post_cover.ejs +1 -1
  9. package/layout/_partial/head.ejs +11 -2
  10. package/layout/_partial/main/article/article_footer.ejs +13 -10
  11. package/layout/_partial/main/navbar/article_banner.ejs +7 -0
  12. package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
  13. package/layout/_partial/main/navbar/nav_tabs_blog.ejs +6 -3
  14. package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +1 -1
  15. package/layout/_partial/main/pin_slider.ejs +11 -12
  16. package/layout/_partial/main/post_list/post_card.ejs +24 -13
  17. package/layout/_partial/sidebar/index_leftbar.ejs +2 -2
  18. package/layout/_partial/widgets/markdown.ejs +1 -1
  19. package/layout/_partial/widgets/toc.ejs +8 -1
  20. package/layout/categories.ejs +18 -8
  21. package/layout/index.ejs +67 -11
  22. package/layout/page.ejs +5 -1
  23. package/package.json +5 -3
  24. package/scripts/commands/new-note.js +2 -2
  25. package/scripts/helpers/category_tree.js +11 -0
  26. package/scripts/helpers/escape_html.js +9 -0
  27. package/scripts/helpers/json_ld.js +5 -0
  28. package/scripts/helpers/mdrender.js +29 -0
  29. package/scripts/helpers/reading_time.js +12 -0
  30. package/scripts/helpers/related_posts.js +9 -5
  31. package/scripts/lib/category_tree.js +26 -0
  32. package/scripts/lib/mdrender_html.js +97 -0
  33. package/scripts/lib/reading_time.js +16 -0
  34. package/scripts/lib/wiki_readme.js +57 -0
  35. package/scripts/tags/index.js +2 -0
  36. package/scripts/tags/lib/md.js +17 -9
  37. package/scripts/tags/lib/table.js +22 -0
  38. package/scripts/tags/lib/timeline.js +9 -9
  39. package/scripts/tags/lib/tip.js +23 -0
  40. package/source/css/_common/device.styl +3 -0
  41. package/source/css/_components/list.styl +11 -5
  42. package/source/css/_components/pages/archives.styl +7 -3
  43. package/source/css/_components/partial/bread-nav.styl +9 -1
  44. package/source/css/_components/sidebar/search.styl +6 -4
  45. package/source/css/_components/sidebar/sidebar.styl +10 -7
  46. package/source/css/_components/tag-plugins/table.styl +37 -0
  47. package/source/css/_components/tag-plugins/tip.styl +27 -0
  48. package/source/css/_custom.styl +1 -1
  49. package/source/css/_defines/func.styl +19 -13
  50. package/source/css/_defines/theme_base.styl +3 -0
  51. package/source/js/main.js +92 -1
  52. package/source/js/services/mdrender.js +116 -7
  53. package/source/js/services/memos.js +60 -8
  54. package/source/js/services/waline_latest_comment.js +3 -1
@@ -63,12 +63,14 @@
63
63
  filter: saturate(var(--saturate)) blur(var(--blur-px)) opacity(var(--background-opacity))
64
64
  :root[data-theme="dark"] &
65
65
  --background-opacity: hexo-config('style.leftbar.background-opacity') * 0.75
66
- @media screen and (prefers-color-scheme: dark)
67
- --background-opacity: hexo-config('style.leftbar.background-opacity') * 0.75
66
+ :root:not([data-theme]) &
67
+ @media screen and (prefers-color-scheme: dark)
68
+ --background-opacity: hexo-config('style.leftbar.background-opacity') * 0.75
68
69
  else if $leftbar-background-color-light || $leftbar-background-color-dark
69
70
  background-color: convert($leftbar-background-color-light)
70
- @media screen and (prefers-color-scheme: dark)
71
- background-color: convert($leftbar-background-color-dark)
71
+ :root:not([data-theme]) &
72
+ @media screen and (prefers-color-scheme: dark)
73
+ background-color: convert($leftbar-background-color-dark)
72
74
 
73
75
  .leftbar-container
74
76
  height: calc(100vh - var(--gap-margin) * 2)
@@ -109,9 +111,10 @@
109
111
  :root[data-theme="dark"] &
110
112
  background: rgba(white, 0.05)
111
113
  box-shadow: inset 0 0 32px 1px rgba(white, 0.1)
112
- @media screen and (prefers-color-scheme: dark)
113
- background: rgba(white, 0.05)
114
- box-shadow: inset 0 0 32px 1px rgba(white, 0.1)
114
+ :root:not([data-theme]) &
115
+ @media screen and (prefers-color-scheme: dark)
116
+ background: rgba(white, 0.05)
117
+ box-shadow: inset 0 0 32px 1px rgba(white, 0.1)
115
118
 
116
119
  @media screen and (max-width: $device-mobile-max)
117
120
  .l_left
@@ -0,0 +1,37 @@
1
+ // 表格标签(#466):{% table style:scroll|wrap|compact %}
2
+ .tag-plugin.table
3
+ table
4
+ border-collapse: collapse
5
+ width: 100%
6
+ &.table-scroll
7
+ overflow-x: auto
8
+ -webkit-overflow-scrolling: touch
9
+ scrollbar()
10
+ table
11
+ min-width: max-content
12
+ th, td
13
+ padding: 8px 1em
14
+ &.table-wrap
15
+ table
16
+ table-layout: fixed
17
+ border-collapse: separate
18
+ border-spacing: 0
19
+ border: 1px solid var(--block-border)
20
+ border-radius: $border-card
21
+ overflow: hidden
22
+ tr
23
+ white-space: normal
24
+ th
25
+ border-top: none
26
+ th, td
27
+ word-break: break-word
28
+ overflow-wrap: anywhere
29
+ padding: 8px 1em
30
+ border-left: 1px solid var(--block-border)
31
+ &:first-child
32
+ border-left: none
33
+ &.table-compact
34
+ table
35
+ th, td
36
+ padding: 4px 1em
37
+ line-height: 1.4
@@ -0,0 +1,27 @@
1
+ // 气泡注解标签(#602):{% tip text:注解 %}词句{% endtip %}
2
+ .tag-plugin.tip
3
+ position: relative
4
+ display: inline-block
5
+ border-bottom: 1px dashed var(--text-p3)
6
+ cursor: help
7
+ .tip-bubble
8
+ position: absolute
9
+ bottom: 135%
10
+ left: 50%
11
+ transform: translateX(-50%)
12
+ display: block
13
+ width: max-content
14
+ max-width: 18rem
15
+ padding: .35rem .7rem
16
+ bar-glass($border-bar)
17
+ text-shadow: none
18
+ color: var(--text-p1)
19
+ font-size: $fs-14
20
+ line-height: 1.5
21
+ white-space: normal
22
+ opacity: 0
23
+ pointer-events: none
24
+ transition: opacity .2s ease
25
+ z-index: 10
26
+ &:hover .tip-bubble, &:focus .tip-bubble
27
+ opacity: 1
@@ -63,7 +63,7 @@ $border-image-l = convert(hexo-config('style.border-radius.image-l'))
63
63
  $border-image = convert(hexo-config('style.border-radius.image'))
64
64
  $border-image-s = convert(hexo-config('style.border-radius.image-s'))
65
65
  $border-button = 8px
66
- $corner-shape = convert(hexo-config('style.corner-shape') || 'superellipse(1.2)')
66
+ $corner-shape = convert(hexo-config('style.corner-shape'))
67
67
 
68
68
  // 可以动态变化的属性
69
69
  :root
@@ -143,9 +143,10 @@ sidebar-light($bg = unquote('var(--bg-a50)'), $light = 0.38, $edge = 0.45, $dark
143
143
  :root[data-theme="dark"] &
144
144
  background: var(--leftbar-item-bg, linear-gradient(180deg, rgba(white, $dark-light), rgba(white, 0) 50%), $bg)
145
145
  box-shadow: var(--leftbar-item-shadow, inset 0 1px 0 rgba(white, $dark-edge))
146
- @media (prefers-color-scheme: dark)
147
- background: var(--leftbar-item-bg, linear-gradient(180deg, rgba(white, $dark-light), rgba(white, 0) 50%), $bg)
148
- box-shadow: var(--leftbar-item-shadow, inset 0 1px 0 rgba(white, $dark-edge))
146
+ :root:not([data-theme]) &
147
+ @media (prefers-color-scheme: dark)
148
+ background: var(--leftbar-item-bg, linear-gradient(180deg, rgba(white, $dark-light), rgba(white, 0) 50%), $bg)
149
+ box-shadow: var(--leftbar-item-shadow, inset 0 1px 0 rgba(white, $dark-edge))
149
150
 
150
151
  newblur($radius = 64px)
151
152
  box-shadow: 0 0 2px rgba(black, .04), 0 2px 8px rgba(black, .04), 0 4px 16px rgba(black, .04)
@@ -153,14 +154,16 @@ newblur($radius = 64px)
153
154
  text-shadow: 0 0 4px rgba(white, 1), 0 0 8px rgba(white, 0.8), 0 0 16px rgba(white, 0.6)
154
155
  :root[data-theme="dark"] &
155
156
  text-shadow: 0 0 2px rgba(black, 0.2), 0 0 4px rgba(black, 0.4)
156
- @media (prefers-color-scheme: dark)
157
- text-shadow: 0 0 2px rgba(black, 0.2), 0 0 4px rgba(black, 0.4)
157
+ :root:not([data-theme]) &
158
+ @media (prefers-color-scheme: dark)
159
+ text-shadow: 0 0 2px rgba(black, 0.2), 0 0 4px rgba(black, 0.4)
158
160
  nav a:not(.active,:hover)
159
161
  filter: drop-shadow(0 0 0 white) drop-shadow(0 0 4px white)
160
162
  :root[data-theme="dark"] &
161
163
  filter: none
162
- @media (prefers-color-scheme: dark)
163
- filter: none
164
+ :root:not([data-theme]) &
165
+ @media (prefers-color-scheme: dark)
166
+ filter: none
164
167
  &:before,&:after
165
168
  z-index -1
166
169
  position: absolute
@@ -179,8 +182,9 @@ newblur($radius = 64px)
179
182
  margin: 1px
180
183
  :root[data-theme="dark"] &
181
184
  background: rgba(white, 0.2)
182
- @media (prefers-color-scheme: dark)
183
- background: rgba(white, 0.2)
185
+ :root:not([data-theme]) &
186
+ @media (prefers-color-scheme: dark)
187
+ background: rgba(white, 0.2)
184
188
  &:after
185
189
  --blur-px: 16px
186
190
  --blur-sat: 300%
@@ -192,8 +196,9 @@ newblur($radius = 64px)
192
196
  trans1 all
193
197
  :root[data-theme="dark"] &
194
198
  box-shadow: inset 0 0 2px 1px rgba(white, 0.2)
195
- @media (prefers-color-scheme: dark)
196
- box-shadow: inset 0 0 2px 1px rgba(white, 0.2)
199
+ :root:not([data-theme]) &
200
+ @media (prefers-color-scheme: dark)
201
+ box-shadow: inset 0 0 2px 1px rgba(white, 0.2)
197
202
 
198
203
  // 长条圆角玻璃 UI(navbar top 与 float-panel 共用)
199
204
  bar-glass($radius = $border-bar-container)
@@ -220,8 +225,9 @@ bar-item-active()
220
225
  background: var(--bg-a60)
221
226
  :root[data-theme="dark"] &
222
227
  background: rgba(white, 0.25)
223
- @media (prefers-color-scheme: dark)
224
- background: rgba(white, 0.25)
228
+ :root:not([data-theme]) &
229
+ @media (prefers-color-scheme: dark)
230
+ background: rgba(white, 0.25)
225
231
  box-shadow: 0 0 1px rgba(0, 0, 0, 0.04), 0 0 2px rgba(0, 0, 0, 0.04), 0 0 4px rgba(0, 0, 0, 0.08)
226
232
  backdrop-filter: saturate(300%)
227
233
  -webkit-backdrop-filter: saturate(300%)
@@ -117,6 +117,9 @@ dynamic-theme-dark()
117
117
 
118
118
  // dynamic colors
119
119
  dynamic-theme-light()
120
+
121
+
122
+ :root:not([data-theme])
120
123
  @media (prefers-color-scheme: dark)
121
124
  dynamic-theme-dark()
122
125
 
package/source/js/main.js CHANGED
@@ -132,14 +132,105 @@ function smoothScrollTo(targetY) {
132
132
  window.addEventListener('wheel', cancelSmoothScroll, { passive: true });
133
133
  window.addEventListener('touchstart', cancelSmoothScroll, { passive: true });
134
134
 
135
+ // 远程 md(mdrender 服务)渲染完成后重建右栏 TOC:结构与服务端 toc() 输出一致
136
+ let tocClickBound = false;
137
+ function rebuildToc(scope) {
138
+ const widget = document.querySelector('#data-toc');
139
+ if (!widget) {
140
+ return;
141
+ }
142
+ const body = widget.querySelector('.widget-body');
143
+ if (!body) {
144
+ return;
145
+ }
146
+ const article = scope && scope.closest ? scope.closest('article.md-text') : null;
147
+ const root = article || document.querySelector('article.md-text');
148
+ if (!root) {
149
+ return;
150
+ }
151
+ const headings = root.querySelectorAll('h1,h2,h3,h4,h5,h6');
152
+ if (headings.length === 0) {
153
+ return;
154
+ }
155
+ const ol = document.createElement('ol');
156
+ ol.className = 'toc';
157
+ const stack = [];
158
+ headings.forEach(function (h) {
159
+ const id = h.id;
160
+ if (!id) {
161
+ return;
162
+ }
163
+ const level = parseInt(h.tagName.substring(1), 10);
164
+ const li = document.createElement('li');
165
+ li.className = 'toc-item toc-level-' + level;
166
+ const a = document.createElement('a');
167
+ a.className = 'toc-link';
168
+ a.href = '#' + encodeURIComponent(id);
169
+ const span = document.createElement('span');
170
+ span.className = 'toc-text';
171
+ span.textContent = h.textContent.trim();
172
+ a.appendChild(span);
173
+ li.appendChild(a);
174
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
175
+ stack.pop();
176
+ }
177
+ if (stack.length === 0) {
178
+ ol.appendChild(li);
179
+ } else {
180
+ const parent = stack[stack.length - 1];
181
+ if (!parent.childOl) {
182
+ parent.childOl = document.createElement('ol');
183
+ parent.childOl.className = 'toc-child';
184
+ parent.li.appendChild(parent.childOl);
185
+ }
186
+ parent.childOl.appendChild(li);
187
+ }
188
+ stack.push({ level: level, li: li });
189
+ });
190
+ body.innerHTML = '';
191
+ body.appendChild(ol);
192
+ bindTocClick(widget);
193
+ }
194
+
195
+ function bindTocClick(widget) {
196
+ if (tocClickBound) {
197
+ return;
198
+ }
199
+ tocClickBound = true;
200
+ widget.addEventListener('click', function (e) {
201
+ const link = e.target.closest('a.toc-link');
202
+ if (!link) {
203
+ return;
204
+ }
205
+ const href = link.getAttribute('href');
206
+ const id = href && href.indexOf('#') === 0 ? decodeURIComponent(href.slice(1)) : null;
207
+ const target = id && document.getElementById(id);
208
+ if (target) {
209
+ e.preventDefault();
210
+ const offset = 32;
211
+ const targetY = target.getBoundingClientRect().top + window.scrollY - offset;
212
+ smoothScrollTo(targetY);
213
+ if (window.history && window.history.pushState) {
214
+ window.history.pushState(null, '', href);
215
+ }
216
+ }
217
+ });
218
+ }
219
+
220
+ // 远程 md 渲染完成后由页面层重建右栏 TOC
221
+ document.addEventListener('stellar:mdrender', function (e) {
222
+ rebuildToc(e.detail && e.detail.target);
223
+ });
224
+
135
225
 
136
226
  const init = {
137
227
  toc: () => {
138
228
  const scrollOffset = 32;
139
229
  // 滚动位置取整后标题顶可能落在偏移线下方 1~2px,加容差避免高亮回跳到上一条
140
230
  const scrollTolerance = 4;
141
- var segs = utils.qsa("article.md-text h1, article.md-text h2, article.md-text h3, article.md-text h4, article.md-text h5, article.md-text h6");
142
231
  function activeTOC() {
232
+ // 每次滚动动态查询:远程 md 内容渲染后标题才存在
233
+ var segs = utils.qsa("article.md-text h1, article.md-text h2, article.md-text h3, article.md-text h4, article.md-text h5, article.md-text h6");
143
234
  var scrollTop = window.scrollY;
144
235
  var topSeg = null;
145
236
  for (var i = 0; i < segs.length; i++) {
@@ -1,12 +1,121 @@
1
+ /* global module */
2
+ /**
3
+ * mdrender.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
4
+ *
5
+ * 远程 Markdown 渲染底层组件的客户端部分:
6
+ * - 无 data-replace:渲染结果填充进占位元素(容器保留,兼容现状);
7
+ * - data-replace="true":占位元素原地替换为渲染结果(最终 DOM 无外部容器);
8
+ * - data-base:相对图片/链接解析到该基址(GitHub 镜像优先);
9
+ * - data-heading="true":标题适配本地文章格式(补 id + headerlink;h1 为页面标题直接隐藏);
10
+ * - 渲染完成后派发 stellar:mdrender 事件(detail.target 为渲染后的占位元素),
11
+ * 页面层(main.js)监听后重建右栏 TOC。
12
+ */
1
13
  (function () {
2
- const els = document.getElementsByClassName('ds-mdrender');
3
- for (var i = 0; i < els.length; i++) {
4
- const el = els[i];
5
- const src = `${el.getAttribute('src')}?t=${new Date().getTime()}`;
6
-
7
- utils.request(el, src, async resp => {
14
+ 'use strict';
15
+
16
+ // 与 hexo-util slugize(transform=1)保持一致:去 HTML、转小写、特殊字符转 -、
17
+ // 连续分隔符合并、首尾分隔符去除(未做 diacritic 归一化,常见标题不受影响)。
18
+ function slugifyHeading(text) {
19
+ return String(text)
20
+ .replace(/<[^>]*>/g, '')
21
+ // eslint-disable-next-line no-control-regex
22
+ .replace(/[\u0000-\u001f]/g, '')
23
+ .replace(/[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g, '-')
24
+ .replace(/-{2,}/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ .toLowerCase();
27
+ }
28
+
29
+ // 标题适配:h1 为页面标题(banner 已展示),直接隐藏不降级;其余标题按序补齐 id(冲突加后缀)
30
+ function normalizeHeadings(container, usedIds) {
31
+ const headings = container.querySelectorAll('h1,h2,h3,h4,h5,h6');
32
+ if (headings.length === 0) {
33
+ return;
34
+ }
35
+ headings.forEach(function (h) {
36
+ if (h.tagName === 'H1') {
37
+ h.remove();
38
+ }
39
+ });
40
+ container.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(function (h) {
41
+ const base = slugifyHeading(h.textContent) || 'section';
42
+ let id = base;
43
+ let suffix = 1;
44
+ while (usedIds.has(id)) {
45
+ id = base + '-' + suffix;
46
+ suffix++;
47
+ }
48
+ usedIds.add(id);
49
+ h.id = id;
50
+ // 与本地 hexo-renderer-marked 输出一致:标题内追加 headerlink 锚点
51
+ const anchor = document.createElement('a');
52
+ anchor.setAttribute('href', '#' + id);
53
+ anchor.className = 'headerlink';
54
+ anchor.setAttribute('title', h.textContent.trim());
55
+ h.insertBefore(anchor, h.firstChild);
56
+ });
57
+ }
58
+
59
+ // 相对图片/链接解析到 data-base(# 锚点、绝对 URL、协议相对地址等不动)
60
+ function rewriteRelativeUrls(container, base) {
61
+ container.querySelectorAll('img[src],a[href]').forEach(function (el) {
62
+ const attr = el.tagName === 'IMG' ? 'src' : 'href';
63
+ const val = el.getAttribute(attr);
64
+ if (!val) {
65
+ return;
66
+ }
67
+ if (/^(#|https?:|mailto:|tel:|data:|javascript:)/i.test(val) || val.startsWith('//')) {
68
+ return;
69
+ }
70
+ try {
71
+ el.setAttribute(attr, new URL(val, base).href);
72
+ } catch (e) {
73
+ // 无法解析的相对地址保持原样
74
+ }
75
+ });
76
+ }
77
+
78
+ function render(el) {
79
+ const src = el.getAttribute('src');
80
+ if (!src) {
81
+ return;
82
+ }
83
+ const url = src + '?t=' + new Date().getTime();
84
+ utils.request(el, url, async resp => {
8
85
  const data = await resp.text();
9
- el.innerHTML = marked.parse(data);
86
+ const tmp = document.createElement('div');
87
+ tmp.innerHTML = marked.parse(data);
88
+ const base = el.getAttribute('data-base');
89
+ if (base) {
90
+ rewriteRelativeUrls(tmp, base);
91
+ }
92
+ if (el.getAttribute('data-heading') === 'true') {
93
+ const usedIds = new Set();
94
+ const scope = el.closest('article, .md-text') || document;
95
+ scope.querySelectorAll('[id]').forEach(function (node) {
96
+ usedIds.add(node.id);
97
+ });
98
+ normalizeHeadings(tmp, usedIds);
99
+ }
100
+ if (el.getAttribute('data-replace') === 'true') {
101
+ el.replaceWith.apply(el, Array.from(tmp.childNodes));
102
+ } else {
103
+ el.innerHTML = tmp.innerHTML;
104
+ }
105
+ document.dispatchEvent(new CustomEvent('stellar:mdrender', { detail: { target: el } }));
10
106
  });
11
107
  }
108
+
109
+ // Node 单测入口:仅导出纯函数,浏览器环境不命中此分支
110
+ if (typeof module !== 'undefined' && module.exports) {
111
+ module.exports = {
112
+ slugifyHeading: slugifyHeading
113
+ };
114
+ return;
115
+ }
116
+
117
+ const els = document.getElementsByClassName('ds-mdrender');
118
+ for (var i = 0; i < els.length; i++) {
119
+ render(els[i]);
120
+ }
12
121
  })();
@@ -112,6 +112,49 @@
112
112
  `<div class="image-bg"><img src="${res.externalLink || `https://${host}/file/${res.name}/${res.filename}`}"></div>`
113
113
  )
114
114
  },
115
+ "v1": {
116
+ buildUser: async (item, memos, default_avatar) => {
117
+ const creator = item?.creator || '';
118
+ const creatorId = creator.split('/')[1] || creator;
119
+ const findUser = () => memos.users.find(user =>
120
+ (user.name || '').split('/')[1] === creatorId || String(user.id) === creatorId
121
+ );
122
+ let user = findUser();
123
+ if (!user && !memos.requests[creatorId]) {
124
+ memos.requests[creatorId] = utils.requestWithoutLoading(`${memos.site}/api/v1/users/${encodeURIComponent(creator)}`, { service: 'memos-user' })
125
+ .then(response => response.json())
126
+ .then(data => {
127
+ if (data.username) {
128
+ memos.users.push(data);
129
+ user = data;
130
+ }
131
+ })
132
+ .catch(() => {})
133
+ .finally(() => delete memos.requests[creatorId]);
134
+ }
135
+ if (memos.requests[creatorId]) {
136
+ await memos.requests[creatorId];
137
+ user = findUser();
138
+ }
139
+ const name = user ? user.displayName || user.username : 'memos';
140
+ const avatarUrl = user?.avatarUrl
141
+ ? (user.avatarUrl.startsWith('http') ? user.avatarUrl : `${memos.site}${user.avatarUrl}`)
142
+ : default_avatar || '';
143
+ return `<div class="user-info">${avatarUrl ? `<img src="${avatarUrl}">` : ''}<span>${name}</span></div>`;
144
+ },
145
+ buildDate: item => {
146
+ const d = new Date(item.createTime);
147
+ return isNaN(d.getTime()) ? new Date() : d;
148
+ },
149
+ buildImages: (item, host) => {
150
+ const list = (item.attachments || item.resources || []).filter(res =>
151
+ res && res.type && String(res.type).includes('image/')
152
+ );
153
+ return list.map(res =>
154
+ `<div class="image-bg"><img src="${res.externalLink || res.url || `https://${host}/file/${res.name}/${res.filename}`}"></div>`
155
+ );
156
+ }
157
+ },
115
158
  "feature": {
116
159
  buildUser: async () => "memos",
117
160
  buildDate: () => new Date(),
@@ -123,14 +166,23 @@
123
166
  memos.version = "22-";
124
167
  memos.data = data;
125
168
  console.log("当前Memos版本为22-");
126
- } else if (data.memos && !data.memos[0].attachments) {
127
- memos.version = "22+";
128
- memos.data = data.memos;
129
- console.log("当前Memos版本为22+");
130
- } else if (data.memos && data.memos[0].attachments) {
131
- memos.version = "25+";
132
- memos.data = data.memos;
133
- console.log("当前Memos版本为25+");
169
+ } else if (data && Array.isArray(data.memos) && data.memos.length > 0) {
170
+ const first = data.memos[0];
171
+ const creatorId = String(first.creator || '').split('/')[1] || '';
172
+ const isV1 = first.createTime != null && creatorId.length > 0 && !/^\d+$/.test(creatorId);
173
+ if (isV1) {
174
+ memos.version = "v1";
175
+ memos.data = data.memos;
176
+ console.log("当前Memos版本为v1");
177
+ } else if (first.attachments) {
178
+ memos.version = "25+";
179
+ memos.data = data.memos;
180
+ console.log("当前Memos版本为25+");
181
+ } else {
182
+ memos.version = "22+";
183
+ memos.data = data.memos;
184
+ console.log("当前Memos版本为22+");
185
+ }
134
186
  } else {
135
187
  memos.version = "feature";
136
188
  console.log("当前Memos版本过高,请到Stellar社区反馈");
@@ -10,7 +10,9 @@
10
10
  const api = apiBase + '/comment?type=recent&count=' + limit;
11
11
  const default_avatar = def.avatar;
12
12
  utils.request(el, api, async resp => {
13
- const data = await resp.json();
13
+ const payload = await resp.json();
14
+ // 兼容旧版数组与新版 { data: [...] } 返回结构(#630)
15
+ const data = Array.isArray(payload) ? payload : (payload && Array.isArray(payload.data) ? payload.data : []);
14
16
  data.forEach((item, i) => {
15
17
  var cell = '<div class="timenode" index="' + i + '">';
16
18
  cell += '<div class="header">';