You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
4.8 KiB
175 lines
4.8 KiB
import "./common.js";
|
|
//#region js/about-history.js
|
|
function setActiveNav() {
|
|
const pageNav = document.querySelector("[data-nav]")?.getAttribute("data-nav");
|
|
if (!pageNav) return;
|
|
document.querySelectorAll(".site-header__nav a").forEach((link) => {
|
|
const active = link.textContent.trim().startsWith(pageNav);
|
|
link.classList.toggle("is-active", active);
|
|
});
|
|
}
|
|
|
|
function getHeaderOffset() {
|
|
const header = document.querySelector(".site-header");
|
|
const h = header ? header.offsetHeight : 88;
|
|
const isMobile = window.matchMedia("(max-width: 992px)").matches;
|
|
if (isMobile) {
|
|
const years = document.querySelector(".ah-nav__years");
|
|
const yearsH = years ? years.offsetHeight : 60;
|
|
return h + yearsH + 12;
|
|
}
|
|
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;
|
|
const navItems = Array.from(root.querySelectorAll(".ah-nav__item[data-year]"));
|
|
const blocks = Array.from(root.querySelectorAll(".ah-block[data-year]"));
|
|
const yearsWrap = root.querySelector(".ah-nav__years");
|
|
const rail = root.querySelector(".ah-nav__rail");
|
|
const indicator = root.querySelector(".ah-nav__indicator");
|
|
if (!navItems.length || !blocks.length) return;
|
|
|
|
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 || isMobile()) return;
|
|
rail.style.height = `${yearsWrap.offsetHeight}px`;
|
|
};
|
|
|
|
const setProgress = (progress) => {
|
|
if (!indicator) return;
|
|
indicator.style.transform = `scaleY(${clamp(progress, 0.04, 1)})`;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* 进度只跟内容滚动的年份序号插值,不依赖左侧字号布局。
|
|
* 跨年时 (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);
|
|
});
|
|
if (scrollNav && isMobile()) {
|
|
const current = navItems.find((el) => el.dataset.year === year);
|
|
current?.scrollIntoView({
|
|
inline: "center",
|
|
block: "nearest",
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
};
|
|
|
|
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;
|
|
clicking = true;
|
|
setActive(year);
|
|
const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset();
|
|
const done = () => {
|
|
clicking = false;
|
|
updateProgressFromScroll();
|
|
};
|
|
if (window.lenis) {
|
|
window.lenis.scrollTo(top, {
|
|
duration: 1,
|
|
onComplete: done
|
|
});
|
|
} else {
|
|
window.scrollTo({
|
|
top,
|
|
behavior: "smooth"
|
|
});
|
|
window.setTimeout(done, 800);
|
|
}
|
|
};
|
|
|
|
navItems.forEach((item) => {
|
|
item.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
scrollToYear(item.dataset.year);
|
|
});
|
|
});
|
|
|
|
if (window.lenis) window.lenis.on("scroll", onScroll);
|
|
else window.addEventListener("scroll", onScroll, { passive: true });
|
|
|
|
if (typeof ResizeObserver !== "undefined" && yearsWrap) {
|
|
new ResizeObserver(() => {
|
|
syncRailHeight();
|
|
updateProgressFromScroll();
|
|
}).observe(yearsWrap);
|
|
}
|
|
|
|
window.addEventListener("resize", () => {
|
|
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));
|
|
}
|
|
}
|
|
|
|
setActiveNav();
|
|
initHistorySpy();
|
|
//#endregion
|