hexo-theme-stellar 1.37.0 → 1.38.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.
- package/CHANGELOG.md +41 -0
- package/_config.yml +13 -8
- package/languages/en.yml +37 -0
- package/languages/zh-CN.yml +37 -0
- package/languages/zh-TW.yml +37 -0
- package/layout/_partial/comments/layout.ejs +2 -1
- package/layout/_partial/main/navbar/article_banner.ejs +11 -5
- package/layout/_partial/main/post_list/post_card.ejs +1 -1
- package/layout/_partial/scripts/defines.ejs +2 -1
- package/layout/_partial/scripts/utils.ejs +58 -15
- package/layout/_partial/sidebar/grad-def.ejs +26 -0
- package/layout/_partial/sidebar/index_leftbar.ejs +25 -10
- package/layout/_partial/sidebar/logo.ejs +21 -0
- package/layout/_partial/sidebar/menu.ejs +11 -5
- package/layout/_partial/widgets/toc.ejs +12 -8
- package/layout/_plugins/scrollreveal.ejs +17 -2
- package/layout/index.ejs +27 -1
- package/layout/notebooks.ejs +3 -1
- package/layout/notes.ejs +3 -1
- package/package.json +1 -1
- package/scripts/tags/index.js +1 -0
- package/scripts/tags/lib/mbti.js +169 -0
- package/source/css/_common/canonical.styl +9 -10
- package/source/css/_common/highlight.styl +0 -2
- package/source/css/_common/html.styl +2 -1
- package/source/css/_common/svg.styl +9 -1
- package/source/css/_components/list.styl +36 -16
- package/source/css/_components/md.styl +0 -1
- package/source/css/_components/pages/article-story.styl +1 -3
- package/source/css/_components/partial/article-banner.styl +17 -11
- package/source/css/_components/partial/article-footer.styl +0 -1
- package/source/css/_components/partial/navbar.styl +3 -3
- package/source/css/_components/sidebar/logo.styl +26 -0
- package/source/css/_components/sidebar/menu.styl +53 -30
- package/source/css/_components/sidebar/nav-area.styl +1 -1
- package/source/css/_components/sidebar/search.styl +46 -7
- package/source/css/_components/sidebar/sidebar.styl +5 -1
- package/source/css/_components/tag-plugins/banner.styl +2 -1
- package/source/css/_components/tag-plugins/chat.styl +3 -1
- package/source/css/_components/tag-plugins/folding.styl +1 -2
- package/source/css/_components/tag-plugins/friends.styl +1 -0
- package/source/css/_components/tag-plugins/friends_posts.styl +2 -1
- package/source/css/_components/tag-plugins/gallery.styl +1 -2
- package/source/css/_components/tag-plugins/grid.styl +1 -2
- package/source/css/_components/tag-plugins/image.styl +0 -2
- package/source/css/_components/tag-plugins/link.styl +0 -1
- package/source/css/_components/tag-plugins/mbti.styl +80 -0
- package/source/css/_components/tag-plugins/note.styl +1 -2
- package/source/css/_components/tag-plugins/okr.styl +1 -2
- package/source/css/_components/tag-plugins/sites.styl +0 -1
- package/source/css/_components/tag-plugins/timeline.styl +1 -6
- package/source/css/_components/widgets/components.styl +14 -7
- package/source/css/_components/widgets/ghuser.styl +2 -1
- package/source/css/_components/widgets/list.styl +20 -8
- package/source/css/_components/widgets/markdown.styl +4 -1
- package/source/css/_components/widgets/related.styl +6 -4
- package/source/css/_components/widgets/toc.styl +2 -2
- package/source/css/_components/widgets/widgets.styl +6 -9
- package/source/css/_custom.styl +2 -1
- package/source/css/_defines/const.styl +1 -1
- package/source/css/_defines/func.styl +13 -1
- package/source/css/_defines/theme_base.styl +22 -1
- package/source/css/_plugins/comments/artalk.styl +2 -2
- package/source/css/_plugins/comments/twikoo.styl +0 -2
- package/source/css/_plugins/tianli_gpt.styl +3 -0
- package/source/js/main.js +39 -17
- package/source/js/services/memos.js +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.38.0
|
|
4
|
+
|
|
5
|
+
> 发布日期:2026-08-11
|
|
6
|
+
|
|
7
|
+
### 新功能
|
|
8
|
+
- 新增连续曲率圆角配置 `style.corner-shape`(默认 `superellipse(1.2)`):所有带圆角的位置统一使用曲率连续的 squircle 曲线,自动适配容器宽高;文章卡片与横幅的渐变模糊改为「同图模糊层」实现以兼容连续曲率(仅 Chromium 139+ 原生支持,其余浏览器自动回退普通圆角,可设 `round` 关闭)
|
|
9
|
+
- 左栏导航列表化,wiki 内页增加返回按钮与搜索条交互优化
|
|
10
|
+
- 菜单栏激活/悬停图标与圆点支持可配置角度的主题色渐变(`style.gradient.angle`)
|
|
11
|
+
- 首页支持可配置评论区(默认关闭,`site_tree.home.comments`)
|
|
12
|
+
- 新增 mbti 标签
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
- 补全 npm-publish workflow 的 GH_TOKEN,修复 GitHub Release 创建失败
|
|
16
|
+
- GitHub Release 正文不再包含 CHANGELOG 发布日期行
|
|
17
|
+
- 克隆站提示源站从 encoded 反解,防止批量替换域名误导
|
|
18
|
+
- 侧栏内部滚动容器组件 scrollreveal 不显示
|
|
19
|
+
- 仅当页面包含 toc 或评论时显示 toc 操作按钮
|
|
20
|
+
- wiki 封面「开始使用」按钮平滑滚动,#start 锚点贴顶定位
|
|
21
|
+
|
|
22
|
+
### 重构
|
|
23
|
+
- 移除官方备用站提示的关闭按钮与配置项
|
|
24
|
+
|
|
25
|
+
### 优化
|
|
26
|
+
- 动态数据组件缓存优化:同 URL 请求去重、memos 用户详情缓存、stale 兜底
|
|
27
|
+
|
|
28
|
+
### 样式
|
|
29
|
+
- 侧栏光照效果覆盖相关卡片、返回胶囊与 widget 操作按钮
|
|
30
|
+
- 侧栏导航区边距调整
|
|
31
|
+
- 左栏激活书签等彩色图标使用主题色渐变
|
|
32
|
+
- 侧栏高亮项顶部叠加白色光照渐变与高光边
|
|
33
|
+
- 左侧栏底部留白调整为 32px
|
|
34
|
+
- 作者页与错误页侧栏不再显示时间线
|
|
35
|
+
- 移动端顶部导航吸顶距离调整为 8px
|
|
36
|
+
|
|
37
|
+
### 升级注意(配置变更与破坏性改动)
|
|
38
|
+
- 新增 `style.corner-shape`(默认 `superellipse(1.2)`):连续曲率圆角仅 Chromium 139+ 生效,其余浏览器自动回退普通圆角;设置 `round` 可关闭
|
|
39
|
+
- 新增 `style.gradient.angle` 与 `site_tree.home.comments` 配置
|
|
40
|
+
- 移除 `canonical.closeEnable`、`canonical.closeText` 配置项:官方备用站提示不再支持手动关闭
|
|
41
|
+
|
|
42
|
+
Full Changelog: [1.37.0...1.38.0](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.37.0...1.38.0)
|
|
43
|
+
|
|
3
44
|
## 1.37.0
|
|
4
45
|
|
|
5
46
|
> 发布日期:2026-08-09
|
package/_config.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
######## Stellar info ########
|
|
2
2
|
stellar:
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.38.0'
|
|
4
4
|
homepage: 'https://xaoxuu.com/wiki/stellar/'
|
|
5
5
|
repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
|
|
6
6
|
main_css: /css/main.css
|
|
@@ -17,8 +17,6 @@ preconnect:
|
|
|
17
17
|
# 一旦设置源站地址,非源站地址将不会被SEO收录,并且访问时弹出提示
|
|
18
18
|
# 如果访问地址不在备用站主机列表,则警告信息为非法克隆
|
|
19
19
|
canonical:
|
|
20
|
-
closeEnable: true # 是否显示关闭提示按钮
|
|
21
|
-
closeText: 关闭提示 # 关闭提示按钮的文字描述
|
|
22
20
|
originalHost: # 主站点域名主机,例如 xaoxuu.com
|
|
23
21
|
officialHosts: # 官方主机列表,每行一个,例如 xaoxuu.vercel.app
|
|
24
22
|
- localhost
|
|
@@ -40,7 +38,6 @@ logo:
|
|
|
40
38
|
|
|
41
39
|
# 侧边栏主功能导航菜单
|
|
42
40
|
menubar:
|
|
43
|
-
columns: 4 # 一行多少个
|
|
44
41
|
items: # 可按照自己需求增加,符合以下格式即可
|
|
45
42
|
# id: 页面中高亮的 menu_id
|
|
46
43
|
# theme: 高亮时的颜色,仅 svg 中 fill="currentColor" 时有效
|
|
@@ -77,6 +74,10 @@ site_tree:
|
|
|
77
74
|
home:
|
|
78
75
|
leftbar: welcome, recent
|
|
79
76
|
rightbar:
|
|
77
|
+
# 首页评论区,默认不显示。可设为 true 使用全局评论服务,
|
|
78
|
+
# 或参考页面 front-matter 覆盖评论参数:
|
|
79
|
+
# service、comment_title、comment_id 及 beaudar/utterances/giscus/twikoo/waline/artalk 各服务参数。
|
|
80
|
+
comments:
|
|
80
81
|
# 博客列表页配置
|
|
81
82
|
index_blog:
|
|
82
83
|
base_dir: blog # 只影响自动生成的页面路径
|
|
@@ -138,14 +139,14 @@ site_tree:
|
|
|
138
139
|
author:
|
|
139
140
|
base_dir: author # 只影响自动生成的页面路径
|
|
140
141
|
menu_id: post
|
|
141
|
-
leftbar: recent
|
|
142
|
-
rightbar:
|
|
142
|
+
leftbar: recent
|
|
143
|
+
rightbar:
|
|
143
144
|
# 错误页配置
|
|
144
145
|
error_page:
|
|
145
146
|
menu_id: post
|
|
146
147
|
'404': '/404.html'
|
|
147
|
-
leftbar: recent
|
|
148
|
-
rightbar:
|
|
148
|
+
leftbar: recent
|
|
149
|
+
rightbar:
|
|
149
150
|
# 其它自定义页面配置 layout: page
|
|
150
151
|
page:
|
|
151
152
|
leftbar: recent
|
|
@@ -530,6 +531,7 @@ data_cache:
|
|
|
530
531
|
waline: 600
|
|
531
532
|
artalk: 600
|
|
532
533
|
memos: 600
|
|
534
|
+
memos-user: 86400
|
|
533
535
|
sites: 86400
|
|
534
536
|
friends: 86400
|
|
535
537
|
friends_and_posts: 86400
|
|
@@ -672,6 +674,7 @@ style:
|
|
|
672
674
|
image-l: 24px # 非技术类文章插图圆角
|
|
673
675
|
image: 16px # 技术类文章插图圆角
|
|
674
676
|
image-s: 8px # 评论区图片圆角(部分评论支持)
|
|
677
|
+
corner-shape: superellipse(1.5) # 连续曲率圆角(仅 Chromium 139+ 支持,其余浏览器自动回退普通圆角):superellipse(1.x) 开启,round 关闭
|
|
675
678
|
color:
|
|
676
679
|
# 主题色配置(不会根据明暗动态调整,请设置为通用的颜色)
|
|
677
680
|
theme: 'hsl(192 98% 55%)' # 主题色
|
|
@@ -688,6 +691,8 @@ style:
|
|
|
688
691
|
error: 加载失败,请稍后重试。
|
|
689
692
|
gradient: # https://webgradients.com/
|
|
690
693
|
start: 'linear-gradient(to right, hsl(215, 95%, 64%), hsl(195, 95%, 60%), hsl(165, 95%, 56%), hsl(165, 95%, 56%), hsl(195 95% 60%), hsl(215, 95%, 64%))'
|
|
694
|
+
searchbar: 'linear-gradient(to right, #04f3ff, #08ffc6, #ddf730, #ffbd19, #ff1fe0, #c418ff, #3b5bff, #04f3ff)' # 搜索条激活时底部横条的渐变色(彩虹,收尾经蓝色平滑过渡)
|
|
695
|
+
angle: 210deg # 主题色渐变的角度(0=向上,顺时针;225deg = 右上角 → 左下角)
|
|
691
696
|
leftbar:
|
|
692
697
|
# 可以设置:纯色/渐变色/图片作为背景
|
|
693
698
|
# 设置为纯色时,可以分别设置浅色和深色模式背景色
|
package/languages/en.yml
CHANGED
|
@@ -68,3 +68,40 @@ symbol:
|
|
|
68
68
|
colon: ": "
|
|
69
69
|
brackets_l: "("
|
|
70
70
|
brackets_r: ")"
|
|
71
|
+
|
|
72
|
+
tag_plugins:
|
|
73
|
+
mbti:
|
|
74
|
+
title: 'My MBTI Type: %s (%s)'
|
|
75
|
+
types:
|
|
76
|
+
INTJ: Architect
|
|
77
|
+
INTP: Logician
|
|
78
|
+
ENTJ: Commander
|
|
79
|
+
ENTP: Debater
|
|
80
|
+
INFJ: Advocate
|
|
81
|
+
INFP: Mediator
|
|
82
|
+
ENFJ: Protagonist
|
|
83
|
+
ENFP: Campaigner
|
|
84
|
+
ISTJ: Logistician
|
|
85
|
+
ISFJ: Defender
|
|
86
|
+
ESTJ: Executive
|
|
87
|
+
ESFJ: Consul
|
|
88
|
+
ISTP: Virtuoso
|
|
89
|
+
ISFP: Adventurer
|
|
90
|
+
ESTP: Entrepreneur
|
|
91
|
+
ESFP: Entertainer
|
|
92
|
+
traits:
|
|
93
|
+
energy:
|
|
94
|
+
left: Extraverted
|
|
95
|
+
right: Introverted
|
|
96
|
+
mind:
|
|
97
|
+
left: Intuitive
|
|
98
|
+
right: Observant
|
|
99
|
+
nature:
|
|
100
|
+
left: Thinking
|
|
101
|
+
right: Feeling
|
|
102
|
+
tactics:
|
|
103
|
+
left: Judging
|
|
104
|
+
right: Prospecting
|
|
105
|
+
identity:
|
|
106
|
+
left: Assertive
|
|
107
|
+
right: Turbulent
|
package/languages/zh-CN.yml
CHANGED
|
@@ -68,3 +68,40 @@ symbol:
|
|
|
68
68
|
colon: ":"
|
|
69
69
|
brackets_l: "("
|
|
70
70
|
brackets_r: ")"
|
|
71
|
+
|
|
72
|
+
tag_plugins:
|
|
73
|
+
mbti:
|
|
74
|
+
title: 我的 MBTI 人格:%s(%s)
|
|
75
|
+
types:
|
|
76
|
+
INTJ: 架构师
|
|
77
|
+
INTP: 逻辑学家
|
|
78
|
+
ENTJ: 指挥官
|
|
79
|
+
ENTP: 辩论家
|
|
80
|
+
INFJ: 提倡者
|
|
81
|
+
INFP: 调停者
|
|
82
|
+
ENFJ: 主人公
|
|
83
|
+
ENFP: 活动家
|
|
84
|
+
ISTJ: 物流师
|
|
85
|
+
ISFJ: 守卫者
|
|
86
|
+
ESTJ: 管理者
|
|
87
|
+
ESFJ: 执政官
|
|
88
|
+
ISTP: 鉴赏家
|
|
89
|
+
ISFP: 冒险家
|
|
90
|
+
ESTP: 企业家
|
|
91
|
+
ESFP: 表演者
|
|
92
|
+
traits:
|
|
93
|
+
energy:
|
|
94
|
+
left: 外向
|
|
95
|
+
right: 内向
|
|
96
|
+
mind:
|
|
97
|
+
left: 天马行空
|
|
98
|
+
right: 求真务实
|
|
99
|
+
nature:
|
|
100
|
+
left: 理性思考
|
|
101
|
+
right: 情感细腻
|
|
102
|
+
tactics:
|
|
103
|
+
left: 运筹帷幄
|
|
104
|
+
right: 随机应变
|
|
105
|
+
identity:
|
|
106
|
+
left: 自信果断
|
|
107
|
+
right: 情绪易波动
|
package/languages/zh-TW.yml
CHANGED
|
@@ -68,3 +68,40 @@ symbol:
|
|
|
68
68
|
colon: ":"
|
|
69
69
|
brackets_l: "("
|
|
70
70
|
brackets_r: ")"
|
|
71
|
+
|
|
72
|
+
tag_plugins:
|
|
73
|
+
mbti:
|
|
74
|
+
title: 我的 MBTI 人格:%s(%s)
|
|
75
|
+
types:
|
|
76
|
+
INTJ: 架構師
|
|
77
|
+
INTP: 邏輯學家
|
|
78
|
+
ENTJ: 指揮官
|
|
79
|
+
ENTP: 辯論家
|
|
80
|
+
INFJ: 提倡者
|
|
81
|
+
INFP: 調停者
|
|
82
|
+
ENFJ: 主人公
|
|
83
|
+
ENFP: 活動家
|
|
84
|
+
ISTJ: 物流師
|
|
85
|
+
ISFJ: 守衛者
|
|
86
|
+
ESTJ: 管理者
|
|
87
|
+
ESFJ: 執政官
|
|
88
|
+
ISTP: 鑑賞家
|
|
89
|
+
ISFP: 冒險家
|
|
90
|
+
ESTP: 企業家
|
|
91
|
+
ESFP: 表演者
|
|
92
|
+
traits:
|
|
93
|
+
energy:
|
|
94
|
+
left: 外向
|
|
95
|
+
right: 內向
|
|
96
|
+
mind:
|
|
97
|
+
left: 天馬行空
|
|
98
|
+
right: 求真務實
|
|
99
|
+
nature:
|
|
100
|
+
left: 理性思考
|
|
101
|
+
right: 情感細膩
|
|
102
|
+
tactics:
|
|
103
|
+
left: 運籌帷幄
|
|
104
|
+
right: 隨機應變
|
|
105
|
+
identity:
|
|
106
|
+
left: 自信果斷
|
|
107
|
+
right: 情緒易波動
|
|
@@ -6,7 +6,7 @@ if (cmt.service) {
|
|
|
6
6
|
cmt[cmt.service] = Object.assign({}, theme.comments[cmt.service]);
|
|
7
7
|
}
|
|
8
8
|
var loadComment = false;
|
|
9
|
-
if (
|
|
9
|
+
if (cmt.service && cmt.service.length > 0) {
|
|
10
10
|
if (page.comments == undefined || page.comments != false) {
|
|
11
11
|
loadComment = true;
|
|
12
12
|
}
|
|
@@ -34,6 +34,7 @@ if (cmt.service && page[cmt.service]) {
|
|
|
34
34
|
Object.assign(cmt[cmt.service], page[cmt.service]);
|
|
35
35
|
}
|
|
36
36
|
page.cmt = cmt;
|
|
37
|
+
page.cmt_rendered = loadComment;
|
|
37
38
|
%>
|
|
38
39
|
<% if (loadComment) { %>
|
|
39
40
|
<div class="related-wrap md-text<%- scrollreveal(' ') %>" id="comments">
|
|
@@ -105,23 +105,29 @@ function layoutDiv() {
|
|
|
105
105
|
return ``
|
|
106
106
|
}
|
|
107
107
|
const top = layoutBreadcrumb()
|
|
108
|
-
var
|
|
108
|
+
var bannerStyle = ``
|
|
109
|
+
var contentStyle = ``
|
|
109
110
|
var el = ``
|
|
110
|
-
|
|
111
|
+
if (banner.url?.length > 0) {
|
|
112
|
+
bannerStyle += ` style="--bg-url:url('${banner.url.replace(/'/g, '%27')}')"`
|
|
113
|
+
}
|
|
114
|
+
el += `<div class="article-banner-wrap${scrollreveal(' ')}">`
|
|
115
|
+
el += `<div class="article banner top"${bannerStyle}>`
|
|
111
116
|
if (banner.url?.length > 0) {
|
|
112
117
|
el += `<img class="lazy bg" data-src="${banner.url}">`
|
|
113
118
|
if (banner.color) {
|
|
114
|
-
|
|
119
|
+
contentStyle += ' style="--text-banner:' + banner.color + '"'
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
el += `
|
|
118
|
-
<div class="content"${
|
|
123
|
+
<div class="content"${contentStyle}>
|
|
119
124
|
${top}
|
|
120
125
|
${bottom}
|
|
121
126
|
</div>
|
|
122
127
|
`
|
|
123
128
|
el += `</div>`
|
|
129
|
+
el += `</div>`
|
|
124
130
|
return el
|
|
125
131
|
}
|
|
126
132
|
%>
|
|
127
|
-
<%- layoutDiv() %>
|
|
133
|
+
<%- layoutDiv() %>
|
|
@@ -109,7 +109,7 @@ function div_photo() {
|
|
|
109
109
|
position = 'bottom';
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
el += '<div class="cover" position="' + position + '">';
|
|
112
|
+
el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + obj.image.replace(/'/g, '%27') + '\')">';
|
|
113
113
|
el += '<img src="' + obj.image + '"/>';
|
|
114
114
|
if (position.length > 0) {
|
|
115
115
|
el += '<div class="cover-info" position="' + position + '"';
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
const anchor = document.getElementById(anchorId);
|
|
33
33
|
if (anchor) {
|
|
34
|
-
|
|
34
|
+
// #start 锚点贴顶,不预留 offset;其余锚点与 TOC 点击滚动保持一致
|
|
35
|
+
const offset = anchorId === 'start' ? 0 : 32;
|
|
35
36
|
const targetY = () => anchor.getBoundingClientRect().top + window.scrollY - offset;
|
|
36
37
|
let timer = null;
|
|
37
38
|
let lastY = null;
|
|
@@ -321,6 +321,31 @@
|
|
|
321
321
|
// 动态数据本地缓存:TTL 未过期直接命中;过期后先显示缓存再后台刷新(stale-while-revalidate)
|
|
322
322
|
_cacheEnabled: true,
|
|
323
323
|
_cacheMaxEntryBytes: 200 * 1024,
|
|
324
|
+
// 同 URL 并发请求去重:页面内多个组件请求同一接口时共享一次 fetch
|
|
325
|
+
_pendingRequests: {},
|
|
326
|
+
_fetchShared: (url, options) => {
|
|
327
|
+
const key = (options && options.method || 'GET') + ' ' + url;
|
|
328
|
+
if (utils._pendingRequests[key]) {
|
|
329
|
+
return utils._pendingRequests[key].then(resp => resp.clone());
|
|
330
|
+
}
|
|
331
|
+
const promise = fetch(url, options).then(resp => {
|
|
332
|
+
if (!resp.ok) throw new Error('响应失败');
|
|
333
|
+
return resp;
|
|
334
|
+
}).finally(() => {
|
|
335
|
+
delete utils._pendingRequests[key];
|
|
336
|
+
});
|
|
337
|
+
utils._pendingRequests[key] = promise;
|
|
338
|
+
// 每个调用方各自 clone,避免 Response body 被消费后其他调用方取不到数据
|
|
339
|
+
return promise.then(resp => resp.clone());
|
|
340
|
+
},
|
|
341
|
+
// 延迟执行:缓存写入等非关键操作让出主线程
|
|
342
|
+
_defer: (fn) => {
|
|
343
|
+
if (window.requestIdleCallback) {
|
|
344
|
+
window.requestIdleCallback(fn, { timeout: 3000 });
|
|
345
|
+
} else {
|
|
346
|
+
setTimeout(fn, 0);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
324
349
|
cache: {
|
|
325
350
|
prefix: 'Stellar.data_cache.v1.',
|
|
326
351
|
enabled: () => !!def.data_cache?.enable && utils._cacheEnabled,
|
|
@@ -500,21 +525,21 @@
|
|
|
500
525
|
}
|
|
501
526
|
}, 5000);
|
|
502
527
|
|
|
503
|
-
|
|
528
|
+
utils._fetchShared(url).then(resp => {
|
|
504
529
|
if (timedOut) return;
|
|
505
530
|
clearTimeout(timeout);
|
|
506
|
-
|
|
507
|
-
if (!resp.ok) throw new Error('响应失败');
|
|
508
531
|
return resp;
|
|
509
532
|
}).then(data => {
|
|
510
533
|
if (timedOut) return;
|
|
511
534
|
// 标记元素实例为已加载
|
|
512
535
|
if (el) utils._loadedElements.add(el);
|
|
513
|
-
//
|
|
536
|
+
// 写入缓存(克隆响应,避免影响回调读取;延迟执行,避免阻塞渲染)
|
|
514
537
|
if (cacheable && data.ok) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
538
|
+
utils._defer(() => {
|
|
539
|
+
data.clone().text().then(text => {
|
|
540
|
+
utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
|
|
541
|
+
}).catch(() => {});
|
|
542
|
+
});
|
|
518
543
|
}
|
|
519
544
|
// 后台刷新渲染前恢复初始结构,避免缓存渲染的内容重复
|
|
520
545
|
if (el && initialHTML !== null) {
|
|
@@ -562,30 +587,48 @@
|
|
|
562
587
|
|
|
563
588
|
return new Promise((resolve, reject) => {
|
|
564
589
|
let retryCount = 0;
|
|
590
|
+
// 与 request 保持一致:最终失败时回退 stale 缓存,避免闪失败态
|
|
591
|
+
const fallback = () => {
|
|
592
|
+
if (cached) {
|
|
593
|
+
resolve(utils.cache.toResponse(cached));
|
|
594
|
+
} else {
|
|
595
|
+
reject('timeout');
|
|
596
|
+
}
|
|
597
|
+
};
|
|
565
598
|
|
|
566
599
|
const tryRequest = () => {
|
|
567
600
|
let timedOut = false;
|
|
568
601
|
const timer = setTimeout(() => {
|
|
569
602
|
timedOut = true;
|
|
570
|
-
if (++retryCount > maxRetry)
|
|
603
|
+
if (++retryCount > maxRetry) fallback();
|
|
571
604
|
else tryRequest();
|
|
572
605
|
}, timeout);
|
|
573
606
|
|
|
574
|
-
|
|
607
|
+
utils._fetchShared(url, options)
|
|
575
608
|
.then(resp => {
|
|
576
609
|
clearTimeout(timer);
|
|
577
|
-
if (
|
|
610
|
+
if (timedOut) return;
|
|
578
611
|
if (cacheable) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
612
|
+
utils._defer(() => {
|
|
613
|
+
resp.clone().text().then(text => {
|
|
614
|
+
utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
|
|
615
|
+
}).catch(() => {});
|
|
616
|
+
});
|
|
582
617
|
}
|
|
583
618
|
resolve(resp);
|
|
584
619
|
})
|
|
585
620
|
.catch(err => {
|
|
586
621
|
clearTimeout(timer);
|
|
587
|
-
if (
|
|
588
|
-
|
|
622
|
+
if (timedOut) return;
|
|
623
|
+
if (++retryCount > maxRetry) {
|
|
624
|
+
if (cached) {
|
|
625
|
+
resolve(utils.cache.toResponse(cached));
|
|
626
|
+
} else {
|
|
627
|
+
reject(err);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
setTimeout(tryRequest, 500);
|
|
631
|
+
}
|
|
589
632
|
});
|
|
590
633
|
};
|
|
591
634
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%
|
|
2
|
+
// 隐藏的 SVG 线性渐变定义,配合 --item-grad 使用(用法见 menu.ejs 与 theme_base.styl 注释)。
|
|
3
|
+
// 渐变角度来自 style.gradient.angle(CSS 角度,0=向上、顺时针),与 CSS 渐变方向保持一致;
|
|
4
|
+
// 颜色使用 --item-theme(默认 var(--theme))与 --item-theme-light 两个 CSS 变量,
|
|
5
|
+
// 因此同一份定义可被任意元素复用,局部覆盖 --item-theme 的元素需同时设置 --item-theme-light。
|
|
6
|
+
var gradAngle = 225
|
|
7
|
+
if (theme.style && theme.style.gradient && theme.style.gradient.angle) {
|
|
8
|
+
gradAngle = parseFloat(theme.style.gradient.angle)
|
|
9
|
+
if (isNaN(gradAngle)) {
|
|
10
|
+
gradAngle = 225
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
var gradRad = gradAngle * Math.PI / 180
|
|
14
|
+
var gradVX = Math.sin(gradRad)
|
|
15
|
+
var gradVY = -Math.cos(gradRad)
|
|
16
|
+
var gradProj = [[0,0],[1,0],[0,1],[1,1]].map(function(c) {
|
|
17
|
+
return c[0] * gradVX + c[1] * gradVY
|
|
18
|
+
})
|
|
19
|
+
var gradMin = Math.min.apply(null, gradProj)
|
|
20
|
+
var gradMax = Math.max.apply(null, gradProj)
|
|
21
|
+
var gradX1 = Math.round((0.5 + gradVX * gradMin) * 1000) / 10
|
|
22
|
+
var gradY1 = Math.round((0.5 + gradVY * gradMin) * 1000) / 10
|
|
23
|
+
var gradX2 = Math.round((0.5 + gradVX * gradMax) * 1000) / 10
|
|
24
|
+
var gradY2 = Math.round((0.5 + gradVY * gradMax) * 1000) / 10
|
|
25
|
+
%>
|
|
26
|
+
<svg class="grad-def" width="0" height="0" aria-hidden="true" focusable="false"><defs><linearGradient id="<%= id %>" x1="<%= gradX1 %>%" y1="<%= gradY1 %>%" x2="<%= gradX2 %>%" y2="<%= gradY2 %>%"><stop offset="20%" style="stop-color:var(--item-theme-light)"/><stop offset="60%" style="stop-color:var(--item-theme)"/></linearGradient></defs></svg>
|
|
@@ -78,16 +78,11 @@ function layoutTitle(main, url, sub) {
|
|
|
78
78
|
function layoutWidgets() {
|
|
79
79
|
var el = '';
|
|
80
80
|
el += '<div class="widgets">';
|
|
81
|
+
// 左栏共用的 SVG 渐变定义(供激活书签等彩色图标使用,见 grad-def.ejs)
|
|
82
|
+
el += partial('grad-def', {id: 'leftbar-active-icon-grad'})
|
|
83
|
+
// 导航区放进组件滚动区,一起滚动
|
|
84
|
+
el += layoutNavArea()
|
|
81
85
|
if (page.leftbar) {
|
|
82
|
-
var search = {}
|
|
83
|
-
if (page.search) {
|
|
84
|
-
search = page.search
|
|
85
|
-
} else if (wiki?.search) {
|
|
86
|
-
search = wiki.search
|
|
87
|
-
} else if (topic?.search) {
|
|
88
|
-
search = topic.search
|
|
89
|
-
}
|
|
90
|
-
el += partial('search', {item: search})
|
|
91
86
|
page.leftbar.forEach((w, i) => {
|
|
92
87
|
let name = ''
|
|
93
88
|
let widget = {}
|
|
@@ -110,6 +105,20 @@ function layoutWidgets() {
|
|
|
110
105
|
el += '</div>';
|
|
111
106
|
return el;
|
|
112
107
|
}
|
|
108
|
+
function layoutSearch() {
|
|
109
|
+
if (!page.leftbar) {
|
|
110
|
+
return ''
|
|
111
|
+
}
|
|
112
|
+
var search = {}
|
|
113
|
+
if (page.search) {
|
|
114
|
+
search = page.search
|
|
115
|
+
} else if (wiki?.search) {
|
|
116
|
+
search = wiki.search
|
|
117
|
+
} else if (topic?.search) {
|
|
118
|
+
search = topic.search
|
|
119
|
+
}
|
|
120
|
+
return partial('search', {item: search})
|
|
121
|
+
}
|
|
113
122
|
function layoutFooterDiv() {
|
|
114
123
|
const { social } = theme.footer
|
|
115
124
|
if (social == null) {
|
|
@@ -154,6 +163,12 @@ function layoutNavArea() {
|
|
|
154
163
|
if (page.menu == false) {
|
|
155
164
|
return el
|
|
156
165
|
}
|
|
166
|
+
// wiki 内容页默认隐藏导航区,page.menu 或 wiki.menu 为 true 时显示
|
|
167
|
+
if (page.wiki?.length > 0) {
|
|
168
|
+
if (page.menu !== true && wiki?.menu !== true) {
|
|
169
|
+
return el
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
el += `<div class="nav-area">`
|
|
158
173
|
el += partial('menu', {where: 'sidebar'})
|
|
159
174
|
el += `</div>`
|
|
@@ -161,6 +176,6 @@ function layoutNavArea() {
|
|
|
161
176
|
}
|
|
162
177
|
%>
|
|
163
178
|
<%- layoutLogo() %>
|
|
164
|
-
<%-
|
|
179
|
+
<%- layoutSearch() %>
|
|
165
180
|
<%- layoutWidgets() %>
|
|
166
181
|
<%- layoutFooterDiv() %>
|
|
@@ -37,6 +37,26 @@ function layoutTitle(main, url, sub) {
|
|
|
37
37
|
return el
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function layoutBack() {
|
|
41
|
+
if (where != 'sidebar' || !page.wiki) {
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
// 开关:page 的 wiki_home 优先,其次 wiki 定义的 wiki_home,默认显示
|
|
45
|
+
const wiki = theme.wiki.tree[page.wiki]
|
|
46
|
+
const enabled = page.wiki_home ?? wiki?.wiki_home ?? true
|
|
47
|
+
if (enabled === false) {
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
var el = ''
|
|
51
|
+
el += `<div class="wiki-home-wrap">`
|
|
52
|
+
el += `<a class="wiki-home cap" href="${pretty_url(theme.site_tree.index_wiki.base_dir)}">`
|
|
53
|
+
el += icon('default:goback')
|
|
54
|
+
el += `<span>${__('btn.all_wiki')}</span>`
|
|
55
|
+
el += `</a>`
|
|
56
|
+
el += `</div>`
|
|
57
|
+
return el
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
function layoutDiv() {
|
|
41
61
|
if (where == 'main') {
|
|
42
62
|
if (page.header == false) {
|
|
@@ -75,4 +95,5 @@ function layoutDiv() {
|
|
|
75
95
|
}
|
|
76
96
|
%>
|
|
77
97
|
|
|
98
|
+
<%- layoutBack() %>
|
|
78
99
|
<%- layoutDiv() %>
|
|
@@ -6,16 +6,22 @@ function layoutDiv() {
|
|
|
6
6
|
if (item == undefined || item.id == null || item.url == null) {
|
|
7
7
|
continue
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
var active = item.id == page.menu_id
|
|
10
|
+
el += `<a class="nav-item${active ? ' active' : ''}${item.theme?.length > 0 ? ' themed' : ''}"`
|
|
11
|
+
var style = `--item-grad:url(#menu-grad-${item.id})`
|
|
10
12
|
if (item.theme?.length > 0) {
|
|
11
|
-
|
|
13
|
+
style += `;--item-theme:${item.theme}`
|
|
12
14
|
}
|
|
13
|
-
el +=
|
|
15
|
+
el += ` style="${style}"`
|
|
16
|
+
el += ` title="${item.title}" href="${pretty_url(item.url)}">`
|
|
14
17
|
if (item.icon?.length > 0) {
|
|
15
18
|
el += icon(item.icon, 'no-lazy')
|
|
16
|
-
} else {
|
|
17
|
-
el += `<span>${__(item.title)}</span>`
|
|
18
19
|
}
|
|
20
|
+
el += `<span>${__(item.title)}</span>`
|
|
21
|
+
if (active) {
|
|
22
|
+
el += `<span class="active-dot"></span>`
|
|
23
|
+
}
|
|
24
|
+
el += partial('grad-def', {id: 'menu-grad-' + item.id})
|
|
19
25
|
el += `</a>`
|
|
20
26
|
}
|
|
21
27
|
el += `</nav>`
|
|
@@ -37,9 +37,15 @@ function layoutToc(fallback) {
|
|
|
37
37
|
|
|
38
38
|
function layoutDiv(fallback) {
|
|
39
39
|
const tocBody = layoutTocBody()
|
|
40
|
+
const hasToc = tocBody.trim().length > 0
|
|
41
|
+
// 页面是否实际渲染评论区,由 _partial/comments/layout.ejs 标记
|
|
42
|
+
const hasComments = page.cmt_rendered === true
|
|
43
|
+
if (!hasToc && !hasComments) {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
40
46
|
var el = ''
|
|
41
47
|
el += `<widget class="widget-wrapper${scrollreveal(' ')} toc" id="data-toc" collapse="${item.collapse}">`
|
|
42
|
-
if (
|
|
48
|
+
if (hasToc) {
|
|
43
49
|
el += layoutTocHeader()
|
|
44
50
|
el += `<div class="widget-body">`
|
|
45
51
|
el += tocBody
|
|
@@ -50,13 +56,11 @@ function layoutDiv(fallback) {
|
|
|
50
56
|
el += icon('default:upup')
|
|
51
57
|
el += `<span>${__('btn.top')}</span>`
|
|
52
58
|
el += `</a>`
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
el += `</a>`
|
|
59
|
-
}
|
|
59
|
+
if (hasComments) {
|
|
60
|
+
el += `<a class="buttom" onclick="util.scrollComment()">`
|
|
61
|
+
el += icon('default:tocomment')
|
|
62
|
+
el += `<span>${__('btn.comments')}</span>`
|
|
63
|
+
el += `</a>`
|
|
60
64
|
}
|
|
61
65
|
el += `</div>`
|
|
62
66
|
el += `</widget>`
|
|
@@ -18,11 +18,26 @@
|
|
|
18
18
|
return null;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// 最近的滚动容器(overflow-y: auto/scroll)
|
|
22
|
+
// 吸顶容器内可能存在内部滚动容器(如侧栏 .widgets),
|
|
23
|
+
// 若直接用吸顶容器做 reveal 容器,折叠区下方的元素永远不会进入视口,导致一直不显示。
|
|
24
|
+
const findScrollContainer = (el) => {
|
|
25
|
+
let node = el.parentElement;
|
|
26
|
+
while (node && node.nodeType === 1) {
|
|
27
|
+
const oy = window.getComputedStyle(node).overflowY;
|
|
28
|
+
if (oy === 'auto' || oy === 'scroll') return node;
|
|
29
|
+
node = node.parentElement;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
21
34
|
const pinnedGroups = new Map(); // 容器 -> 元素列表
|
|
22
35
|
const targets = [];
|
|
23
36
|
els.forEach((el) => {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
37
|
+
const pinned = findPinnedContainer(el);
|
|
38
|
+
if (pinned) {
|
|
39
|
+
// 优先使用最近的滚动容器,其次才用吸顶容器
|
|
40
|
+
const container = findScrollContainer(el) || pinned;
|
|
26
41
|
const group = pinnedGroups.get(container) || [];
|
|
27
42
|
group.push(el);
|
|
28
43
|
pinnedGroups.set(container, group);
|