@@ -160,6 +160,91 @@ const CodeCopy = {
160
160
}
161
161
} ;
162
162
163
+ // Banner 和 Header 高度管理模块
164
+ const Banner = {
165
+ updateHeaderHeight ( ) {
166
+ requestAnimationFrame ( ( ) => {
167
+ const header = DOMUtils . find ( 'header nav' ) ;
168
+ if ( header ) {
169
+ const height = header . offsetHeight ;
170
+ document . documentElement . style . setProperty ( '--header-height' , height + 'px' ) ;
171
+
172
+ // 更新 sidebar 和 toc 的 top 位置
173
+ const sidebar = DOMUtils . find ( '#sidebar-menu nav' ) ;
174
+ const toc = DOMUtils . find ( '#toc-container nav' ) ;
175
+
176
+ if ( sidebar ) {
177
+ // 更新 sidebar 的 top 位置
178
+ sidebar . style . top = `calc(var(--header-height) + 1rem)` ;
179
+ }
180
+
181
+ if ( toc ) {
182
+ // 更新 toc 的 top 位置
183
+ toc . style . top = `calc(var(--header-height) + 1rem)` ;
184
+ }
185
+ }
186
+ } ) ;
187
+ } ,
188
+
189
+ close ( button ) {
190
+ const banner = button . closest ( '[data-banner]' ) ;
191
+ if ( ! banner ) {
192
+ return ;
193
+ }
194
+
195
+ // 移除之前的监听器
196
+ if ( window . _bannerObserver ) {
197
+ window . _bannerObserver . disconnect ( ) ;
198
+ }
199
+
200
+ // 添加过渡效果
201
+ banner . style . transition = 'height 0.2s ease-out, opacity 0.2s ease-out' ;
202
+ banner . style . height = banner . offsetHeight + 'px' ;
203
+ banner . style . opacity = '1' ;
204
+
205
+ // 强制重绘
206
+ banner . offsetHeight ;
207
+
208
+ // 开始动画
209
+ banner . style . height = '0' ;
210
+ banner . style . opacity = '0' ;
211
+ banner . style . overflow = 'hidden' ;
212
+
213
+ // 动画结束后移除元素并更新高度
214
+ banner . addEventListener ( 'transitionend' , function handler ( ) {
215
+ banner . remove ( ) ;
216
+ Banner . updateHeaderHeight ( ) ;
217
+ banner . removeEventListener ( 'transitionend' , handler ) ;
218
+ } ) ;
219
+ } ,
220
+
221
+ init ( ) {
222
+ // 初始化 header 高度
223
+ this . updateHeaderHeight ( ) ;
224
+
225
+ // 监听窗口大小变化
226
+ window . addEventListener ( 'resize' , ( ) => this . updateHeaderHeight ( ) ) ;
227
+
228
+ // 监听 banner 的变化
229
+ const banner = DOMUtils . find ( '[data-banner]' ) ;
230
+ if ( banner ) {
231
+ window . _bannerObserver = new MutationObserver ( ( ) => this . updateHeaderHeight ( ) ) ;
232
+ window . _bannerObserver . observe ( banner , {
233
+ attributes : true ,
234
+ childList : true ,
235
+ subtree : true ,
236
+ characterData : true
237
+ } ) ;
238
+
239
+ // 绑定关闭按钮事件
240
+ const closeButton = DOMUtils . find ( '[data-banner-close]' , banner ) ;
241
+ if ( closeButton ) {
242
+ closeButton . addEventListener ( 'click' , ( ) => this . close ( closeButton ) ) ;
243
+ }
244
+ }
245
+ }
246
+ } ;
247
+
163
248
// Header 组件
164
249
const Header = {
165
250
DarkMode : {
@@ -280,6 +365,7 @@ const Header = {
280
365
init ( ) {
281
366
this . DarkMode . init ( ) ;
282
367
this . initEventListeners ( ) ;
368
+ Banner . init ( ) ;
283
369
284
370
const observer = new MutationObserver ( ( ) => {
285
371
this . initEventListeners ( ) ;
@@ -331,19 +417,174 @@ const FontSizeControl = {
331
417
}
332
418
} ;
333
419
420
+ const TOC = {
421
+ init ( ) {
422
+ // 添加样式
423
+ if ( ! document . getElementById ( 'toc-styles' ) ) {
424
+ const style = document . createElement ( 'style' ) ;
425
+ style . id = 'toc-styles' ;
426
+ style . textContent = `
427
+ .toc-link.active-toc-item {
428
+ background-color: rgb(239 246 255);
429
+ color: rgb(37 99 235);
430
+ position: relative;
431
+ }
432
+ .dark .toc-link.active-toc-item {
433
+ background-color: rgba(30 58 138 / 0.2);
434
+ color: rgb(96 165 250);
435
+ }
436
+ .toc-link.active-toc-item::before {
437
+ content: '';
438
+ position: absolute;
439
+ left: 0;
440
+ top: 50%;
441
+ transform: translateY(-50%);
442
+ width: 3px;
443
+ height: 16px;
444
+ background-color: rgb(37 99 235);
445
+ border-top-right-radius: 2px;
446
+ border-bottom-right-radius: 2px;
447
+ }
448
+ .dark .toc-link.active-toc-item::before {
449
+ background-color: rgb(96 165 250);
450
+ }
451
+ ` ;
452
+ document . head . appendChild ( style ) ;
453
+ }
454
+
455
+ // 获取所有 TOC 链接
456
+ const tocLinks = document . querySelectorAll ( '.toc-link' ) ;
457
+
458
+ // 处理点击事件
459
+ tocLinks . forEach ( link => {
460
+ link . addEventListener ( 'click' , ( e ) => {
461
+ e . preventDefault ( ) ;
462
+
463
+ // 移除所有活跃状态
464
+ tocLinks . forEach ( l => l . classList . remove ( 'active-toc-item' ) ) ;
465
+
466
+ // 添加当前项的活跃状态
467
+ link . classList . add ( 'active-toc-item' ) ;
468
+
469
+ // 获取目标元素和滚动位置
470
+ const targetId = link . getAttribute ( 'data-slug' ) ;
471
+ const target = document . getElementById ( targetId ) ;
472
+ const header = document . querySelector ( 'header nav' ) ;
473
+ const offset = header ? header . offsetHeight + 10 : 80 ;
474
+ const targetPosition = target . getBoundingClientRect ( ) . top + window . pageYOffset - offset ;
475
+
476
+ // 滚动到目标位置
477
+ window . scrollTo ( {
478
+ top : targetPosition ,
479
+ behavior : 'smooth'
480
+ } ) ;
481
+
482
+ // 如果是移动端,关闭 TOC 面板
483
+ const tocMobile = document . getElementById ( 'toc-mobile' ) ;
484
+ if ( tocMobile ) {
485
+ tocMobile . classList . add ( 'translate-y-full' ) ;
486
+ }
487
+ } ) ;
488
+ } ) ;
489
+
490
+ // 监听滚动,更新当前活跃项
491
+ const updateActiveItem = ( ) => {
492
+ const header = document . querySelector ( 'header nav' ) ;
493
+ const headerHeight = header ? header . offsetHeight : 0 ;
494
+
495
+ // 查找所有带 slug 的容器
496
+ const headings = Array . from ( document . querySelectorAll ( '[id].inline-flex' ) ) ;
497
+
498
+ // 找到当前视窗中最靠上的标题
499
+ let current = null ;
500
+ let minDistance = Infinity ;
501
+
502
+ headings . forEach ( heading => {
503
+ const rect = heading . getBoundingClientRect ( ) ;
504
+ const top = rect . top - headerHeight - 20 ;
505
+
506
+ // 计算到视窗顶部的距离
507
+ const distance = Math . abs ( top ) ;
508
+
509
+ // 如果元素在视窗上方或接近顶部,且距离比当前最小距离更小
510
+ if ( top <= 10 && distance < minDistance ) {
511
+ minDistance = distance ;
512
+ current = heading ;
513
+ }
514
+ } ) ;
515
+
516
+ // 更新 TOC 活跃状态
517
+ if ( current ) {
518
+ const slug = current . id ;
519
+ const tocLinks = document . querySelectorAll ( '.toc-link' ) ;
520
+
521
+ tocLinks . forEach ( link => {
522
+ if ( link . getAttribute ( 'data-slug' ) === slug ) {
523
+ link . classList . add ( 'active-toc-item' ) ;
524
+
525
+ // 确保活跃项在滚动区域内可见
526
+ const nav = link . closest ( '.overflow-y-auto' ) ;
527
+ if ( nav ) {
528
+ const navRect = nav . getBoundingClientRect ( ) ;
529
+ const linkRect = link . getBoundingClientRect ( ) ;
530
+
531
+ if ( linkRect . top < navRect . top || linkRect . bottom > navRect . bottom ) {
532
+ link . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ;
533
+ }
534
+ }
535
+ }
536
+ else {
537
+ link . classList . remove ( 'active-toc-item' ) ;
538
+ }
539
+ } ) ;
540
+ }
541
+ } ;
542
+
543
+ // 使用 IntersectionObserver 来优化滚动监听
544
+ const observer = new IntersectionObserver ( ( entries ) => {
545
+ entries . forEach ( ( ) => {
546
+ requestAnimationFrame ( updateActiveItem ) ;
547
+ } ) ;
548
+ } , {
549
+ rootMargin : '-20% 0px -80% 0px' ,
550
+ threshold : [ 0 , 1 ]
551
+ } ) ;
552
+
553
+ // 观察所有标题元素
554
+ document . querySelectorAll ( '[id].inline-flex' ) . forEach ( heading => {
555
+ observer . observe ( heading ) ;
556
+ } ) ;
557
+
558
+ // 仍然保留滚动监听作为备份
559
+ let scrollTimeout ;
560
+ window . addEventListener ( 'scroll' , ( ) => {
561
+ if ( scrollTimeout ) {
562
+ window . cancelAnimationFrame ( scrollTimeout ) ;
563
+ }
564
+ scrollTimeout = window . requestAnimationFrame ( updateActiveItem ) ;
565
+ } , { passive : true } ) ;
566
+
567
+ // 初始调用一次
568
+ updateActiveItem ( ) ;
569
+ }
570
+ } ;
571
+
334
572
// 暴露到全局
335
573
window . PageForge = {
336
574
GitHubStats,
337
575
CodeCopy,
338
576
Header,
339
- FontSizeControl
577
+ FontSizeControl,
578
+ Banner,
579
+ TOC
340
580
} ;
341
581
342
582
// DOM 加载完成后初始化
343
583
if ( document . readyState === 'loading' ) {
344
584
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
345
585
Header . init ( ) ;
346
586
FontSizeControl . init ( ) ;
587
+ TOC . init ( ) ;
347
588
} ) ;
348
589
}
349
590
else {
0 commit comments