feat(dividing): sticky
这个提交包含在:
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -3,7 +3,7 @@ interface MenuState {
|
||||
handleToggleMenu(): void;
|
||||
}
|
||||
|
||||
export const menu = (): MenuState => ({
|
||||
export const headerMenu = (): MenuState => ({
|
||||
isOpen: false,
|
||||
|
||||
handleToggleMenu() {
|
||||
+4
-2
@@ -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()
|
||||
|
||||
|
||||
+44
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<img class="icon" th:if="${not #strings.isEmpty(theme.config.basic.logo)}" th:src="${theme.config.basic.logo}" alt="Logo" />
|
||||
<div class="text" th:if="${not #strings.isEmpty(theme.config.basic.title)}" th:text="${theme.config.basic.title}"></div>
|
||||
</a>
|
||||
<div class="dividing"></div>
|
||||
<div class="header__center" x-data="headerDividing()">
|
||||
<div class="dividing" :class="{ 'sticky': isSticky }"></div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@@ -47,7 +49,7 @@
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<ul th:if="${menu != null} and ${not #lists.isEmpty(menu.menuItems)}" class="menu__inner">
|
||||
<li th:each="menuItem : ${menu.menuItems}" x-data="menu()" @mouseenter="handleToggleMenu()" @mouseleave="handleToggleMenu()">
|
||||
<li th:each="menuItem : ${menu.menuItems}" x-data="headerMenu()" @mouseenter="handleToggleMenu()" @mouseleave="handleToggleMenu()">
|
||||
<a
|
||||
class="text-gray-600 hover:text-blue-600"
|
||||
th:href="${menuItem.status.href}"
|
||||
|
||||
在新议题中引用
屏蔽一个用户