From d5ebbe2c1540242012f00880fbff322babac8e6c Mon Sep 17 00:00:00 2001 From: peijunlei Date: Wed, 9 Sep 2026 09:34:52 +0800 Subject: [PATCH] =?UTF-8?q?style(about-history):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist_static/static/css/about-history.css | 24 ++-- .../themes/dist_static/static/js/about-history.js | 124 ++++++++++++--------- 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/public/themes/dist_static/static/css/about-history.css b/public/themes/dist_static/static/css/about-history.css index ea51471..3488d32 100644 --- a/public/themes/dist_static/static/css/about-history.css +++ b/public/themes/dist_static/static/css/about-history.css @@ -57,7 +57,9 @@ font-weight: 400; line-height: var(--font-24); text-decoration: none; - transition: color 0.25s ease, font-size 0.25s ease, font-weight 0.25s ease; + transform: scale(1); + transform-origin: right center; + transition: color 0.25s ease, transform 0.25s ease, font-weight 0.25s ease; } .ah-nav__item:hover { @@ -65,10 +67,12 @@ } .ah-nav__item.is-active { + z-index: 1; color: var(--color-text); - font-size: var(--font-64); font-weight: 500; - line-height: 1; + /* 约等于原 font-64 / font-24,不占布局,避免进度条回弹 */ + transform: scale(calc(64 / 24)); + margin: 1rem 0; } .ah-nav__rail { @@ -94,18 +98,18 @@ background: var(--color-primary); } -/* 红线:从原点向下变长,不位移 */ +/* 红线:铺满轨道高度,用 scaleY 表示进度(合成层,不触发布局) */ .ah-nav__indicator { position: absolute; - left: 50%; top: 0; + left: 0; width: 2px; - height: 0; - margin-left: -1px; + height: 100%; background: var(--color-primary); border-radius: 1px; - transform: none; - transition: height 0.3s ease; + transform: scaleY(0.04); + transform-origin: top center; + will-change: transform; } .ah-content { @@ -255,9 +259,7 @@ .ah-nav__item.is-active { color: var(--color-text); - font-size: var(--font-18); font-weight: 500; - line-height: 1; transform: scale(1.45); transform-origin: center; } diff --git a/public/themes/dist_static/static/js/about-history.js b/public/themes/dist_static/static/js/about-history.js index 85b0b0f..9e26703 100644 --- a/public/themes/dist_static/static/js/about-history.js +++ b/public/themes/dist_static/static/js/about-history.js @@ -21,6 +21,10 @@ function getHeaderOffset() { return h + 24; } +function clamp(n, min, max) { + return Math.min(max, Math.max(min, n)); +} + function initHistorySpy() { const root = document.querySelector("[data-ah-spy]"); if (!root) return; @@ -33,43 +37,62 @@ function initHistorySpy() { let activeYear = navItems[0].dataset.year; let clicking = false; + let rafId = 0; + + const isMobile = () => window.matchMedia("(max-width: 992px)").matches; const syncRailHeight = () => { - if (!yearsWrap || !rail) return; + if (!yearsWrap || !rail || isMobile()) return; rail.style.height = `${yearsWrap.offsetHeight}px`; }; - const updateLine = (item) => { - if (!indicator || !item || !yearsWrap) return; - syncRailHeight(); - const itemRect = item.getBoundingClientRect(); - const yearsRect = yearsWrap.getBoundingClientRect(); - // 原点固定在顶部,线条延伸到当前高亮年份底部 - const height = Math.max(itemRect.bottom - yearsRect.top, 8); - indicator.style.height = `${height}px`; - indicator.style.transform = ""; + const setProgress = (progress) => { + if (!indicator) return; + indicator.style.transform = `scaleY(${clamp(progress, 0.04, 1)})`; }; - const updateLineSoon = (item) => { - updateLine(item); - requestAnimationFrame(() => updateLine(item)); - window.setTimeout(() => updateLine(item), 320); + const getActiveIndex = () => { + const offset = getHeaderOffset() + 8; + let idx = 0; + for (let i = 0; i < blocks.length; i++) { + if (blocks[i].getBoundingClientRect().top <= offset) idx = i; + } + return idx; }; - const setActive = (year) => { - if (!year || year === activeYear) { - const current = navItems.find((el) => el.dataset.year === activeYear); - updateLineSoon(current); + /** + * 进度只跟内容滚动的年份序号插值,不依赖左侧字号布局。 + * 跨年时 (idx+local)/(n-1) 单调连续,避免「先变短再变长」。 + */ + const updateProgressFromScroll = () => { + if (!indicator || !rail || isMobile()) return; + syncRailHeight(); + const n = navItems.length; + if (n <= 1) { + setProgress(1); return; } + const offset = getHeaderOffset() + 8; + const idx = getActiveIndex(); + let local = 1; + if (blocks[idx + 1]) { + const curTop = blocks[idx].getBoundingClientRect().top; + const nextTop = blocks[idx + 1].getBoundingClientRect().top; + const span = nextTop - curTop; + local = span > 0 ? clamp((offset - curTop) / span, 0, 1) : 1; + } + setProgress((idx + local) / (n - 1)); + }; + + const setActive = (year, { scrollNav = true } = {}) => { + if (!year || year === activeYear) return; activeYear = year; navItems.forEach((el) => { el.classList.toggle("is-active", el.dataset.year === year); }); - const current = navItems.find((el) => el.dataset.year === year); - updateLineSoon(current); - if (current && current.closest(".ah-nav__years") && window.matchMedia("(max-width: 992px)").matches) { - current.scrollIntoView({ + if (scrollNav && isMobile()) { + const current = navItems.find((el) => el.dataset.year === year); + current?.scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" @@ -77,6 +100,20 @@ function initHistorySpy() { } }; + const onScroll = () => { + if (rafId) return; + rafId = requestAnimationFrame(() => { + rafId = 0; + // 点击滚动过程中仍跟手更新进度;仅锁定高亮年份,避免中途抢焦点 + if (!clicking) { + const idx = getActiveIndex(); + const year = navItems[idx]?.dataset.year; + if (year) setActive(year); + } + updateProgressFromScroll(); + }); + }; + const scrollToYear = (year) => { const target = blocks.find((el) => el.dataset.year === year); if (!target) return; @@ -85,6 +122,7 @@ function initHistorySpy() { const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset(); const done = () => { clicking = false; + updateProgressFromScroll(); }; if (window.lenis) { window.lenis.scrollTo(top, { @@ -107,39 +145,25 @@ function initHistorySpy() { }); }); - const io = new IntersectionObserver((entries) => { - if (clicking) return; - const visible = entries.filter((e) => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio); - if (!visible.length) return; - const year = visible[0].target.getAttribute("data-year"); - if (year) setActive(year); - }, { - root: null, - rootMargin: `-${getHeaderOffset()}px 0px -45% 0px`, - threshold: [0.05, 0.2, 0.4, 0.6] - }); - - blocks.forEach((block) => io.observe(block)); + if (window.lenis) window.lenis.on("scroll", onScroll); + else window.addEventListener("scroll", onScroll, { passive: true }); - const onScrollFallback = () => { - if (clicking) return; - const offset = getHeaderOffset() + 8; - let current = blocks[0]?.dataset.year; - for (const block of blocks) { - if (block.getBoundingClientRect().top <= offset) current = block.dataset.year; - } - if (current) setActive(current); - }; - - if (window.lenis) window.lenis.on("scroll", onScrollFallback); - else window.addEventListener("scroll", onScrollFallback, { passive: true }); + if (typeof ResizeObserver !== "undefined" && yearsWrap) { + new ResizeObserver(() => { + syncRailHeight(); + updateProgressFromScroll(); + }).observe(yearsWrap); + } - setActive(activeYear); window.addEventListener("resize", () => { - const current = navItems.find((el) => el.dataset.year === activeYear); - updateLineSoon(current); + syncRailHeight(); + updateProgressFromScroll(); }); + navItems[0]?.classList.add("is-active"); + syncRailHeight(); + updateProgressFromScroll(); + const hashYear = (location.hash || "").replace(/^#year-/, ""); if (hashYear && blocks.some((el) => el.dataset.year === hashYear)) { requestAnimationFrame(() => scrollToYear(hashYear));