diff --git a/src/alpine/header-dividing.ts b/src/alpine/header-dividing.ts
new file mode 100644
index 0000000..a164afa
--- /dev/null
+++ b/src/alpine/header-dividing.ts
@@ -0,0 +1,35 @@
+export const headerDividing = () => ({
+ isSticky: false,
+ $el: null as HTMLElement | null,
+ _observer: null as IntersectionObserver | null,
+
+ init() {
+ const centerEl = this.$el as HTMLElement;
+ if (!centerEl) return;
+
+ const header = centerEl.closest('.header') as HTMLElement;
+
+ if (!header) return;
+
+ // 直接观察 header 元素本身
+ this._observer = new IntersectionObserver(
+ (entries) => {
+ const entry = entries[0];
+ this.isSticky = entry.boundingClientRect.top < 0 && !entry.isIntersecting;
+ },
+ {
+ threshold: 0, // 只要有任何部分离开就触发
+ rootMargin: '0px' // 无偏移
+ }
+ );
+
+ this._observer.observe(header);
+ },
+
+ destroy() {
+ if (this._observer) {
+ this._observer.disconnect();
+ this._observer = null;
+ }
+ }
+});
diff --git a/src/alpine/menu.ts b/src/alpine/header-menu.ts
similarity index 76%
rename from src/alpine/menu.ts
rename to src/alpine/header-menu.ts
index c6167df..6b79901 100644
--- a/src/alpine/menu.ts
+++ b/src/alpine/header-menu.ts
@@ -3,7 +3,7 @@ interface MenuState {
handleToggleMenu(): void;
}
-export const menu = (): MenuState => ({
+export const headerMenu = (): MenuState => ({
isOpen: false,
handleToggleMenu() {
diff --git a/src/main.ts b/src/main.ts
index f71aaf3..d1d0ffe 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,18 +5,20 @@ import "./styles/font-pixel.scss";
import Alpine from 'alpinejs'
import {upvote} from './alpine/upvote'
import {themeMode} from './alpine/theme-mode'
-import {menu} from './alpine/menu'
import {postLineNum} from './alpine/post-line-num'
import {postToc} from './alpine/post-toc'
+import {headerDividing} from './alpine/header-dividing'
+import {headerMenu} from './alpine/header-menu'
import {typewriterEffect} from './utils'
window.Alpine = Alpine
Alpine.data('upvote', upvote)
Alpine.data('themeMode', themeMode)
-Alpine.data('menu', menu)
Alpine.data('postLineNum', postLineNum)
Alpine.data('postToc', postToc)
+Alpine.data('headerMenu', headerMenu)
+Alpine.data('headerDividing', headerDividing)
Alpine.start()
diff --git a/src/styles/header.scss b/src/styles/header.scss
index 52850fc..85b8168 100644
--- a/src/styles/header.scss
+++ b/src/styles/header.scss
@@ -11,12 +11,21 @@
justify-content: space-between;
}
+ &__center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ }
+
.dividing {
flex: 1;
display: block;
width: 100%;
height: 25px;
position: relative;
+ --dash-spacing: 10px;
+ --dash-width: 2px;
// 虚线背景层 - 带遮罩
&::before {
@@ -29,11 +38,11 @@
background: repeating-linear-gradient(
90deg,
var(--foreground),
- var(--foreground) 2px,
+ var(--foreground) var(--dash-width),
transparent 0,
- transparent 10px
+ transparent var(--dash-spacing)
);
- background-size: 10px 100%;
+ background-size: var(--dash-spacing) 100%;
animation: line-flow 3s linear infinite;
// 遮罩层实现虚线淡入淡出效果
@@ -73,6 +82,37 @@
animation: scan-line 5s ease-in-out infinite;
pointer-events: none;
z-index: 10;
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ &.sticky {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 5px;
+ z-index: 100;
+ --dash-spacing: 6px;
+ --dash-width: 2px;
+ background: var(--background);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ &::before {
+ animation: line-flow 1.5s linear infinite;
+ mask-image: none;
+ -webkit-mask-image: none;
+ top: calc(50% - 1px);
+ height: 2px;
+ }
+
+ &::after {
+ animation: scan-line 2.5s ease-in-out infinite;
+ opacity: 0.6;
+ filter: blur(3px);
+ // 调整扫描线高度以适应 5px 容器
+ height: 200%;
+ width: 25%;
+ }
}
}
@@ -82,7 +122,7 @@
background-position: 0 0;
}
100% {
- background-position: 10px 0;
+ background-position: var(--dash-spacing) 0;
}
}
diff --git a/templates/modules/header.html b/templates/modules/header.html
index 31c398c..5d674ec 100644
--- a/templates/modules/header.html
+++ b/templates/modules/header.html
@@ -5,7 +5,9 @@