@@ -417,20 +417,174 @@ const FontSizeControl = {
417
417
}
418
418
} ;
419
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
+
420
572
// 暴露到全局
421
573
window . PageForge = {
422
574
GitHubStats,
423
575
CodeCopy,
424
576
Header,
425
577
FontSizeControl,
426
- Banner
578
+ Banner,
579
+ TOC
427
580
} ;
428
581
429
582
// DOM 加载完成后初始化
430
583
if ( document . readyState === 'loading' ) {
431
584
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
432
585
Header . init ( ) ;
433
586
FontSizeControl . init ( ) ;
587
+ TOC . init ( ) ;
434
588
} ) ;
435
589
}
436
590
else {
0 commit comments