diff --git a/package.json b/package.json index 3719939..c0ca8bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pageforge", - "version": "2025.1.3", + "version": "2025.1.4", "description": "PageForge 是一款现代化的静态页面生成与部署平台,旨在帮助用户快速创建精美的静态网站,并一键部署到 GitHub Pages。 无论是个人博客、项目文档还是企业官网,PageForge 都能让你轻松实现高效构建、智能部署和即时上线。", "homepage": "https://pageforge.devlive.org", "repository": { diff --git a/templates/assets/js/pageforge.js b/templates/assets/js/pageforge.js index 0981a7d..4cd0bfd 100644 --- a/templates/assets/js/pageforge.js +++ b/templates/assets/js/pageforge.js @@ -160,6 +160,91 @@ const CodeCopy = { } }; +// Banner 和 Header 高度管理模块 +const Banner = { + updateHeaderHeight() { + requestAnimationFrame(() => { + const header = DOMUtils.find('header nav'); + if (header) { + const height = header.offsetHeight; + document.documentElement.style.setProperty('--header-height', height + 'px'); + + // 更新 sidebar 和 toc 的 top 位置 + const sidebar = DOMUtils.find('#sidebar-menu nav'); + const toc = DOMUtils.find('#toc-container nav'); + + if (sidebar) { + // 更新 sidebar 的 top 位置 + sidebar.style.top = `calc(var(--header-height) + 1rem)`; + } + + if (toc) { + // 更新 toc 的 top 位置 + toc.style.top = `calc(var(--header-height) + 1rem)`; + } + } + }); + }, + + close(button) { + const banner = button.closest('[data-banner]'); + if (!banner) { + return; + } + + // 移除之前的监听器 + if (window._bannerObserver) { + window._bannerObserver.disconnect(); + } + + // 添加过渡效果 + banner.style.transition = 'height 0.2s ease-out, opacity 0.2s ease-out'; + banner.style.height = banner.offsetHeight + 'px'; + banner.style.opacity = '1'; + + // 强制重绘 + banner.offsetHeight; + + // 开始动画 + banner.style.height = '0'; + banner.style.opacity = '0'; + banner.style.overflow = 'hidden'; + + // 动画结束后移除元素并更新高度 + banner.addEventListener('transitionend', function handler() { + banner.remove(); + Banner.updateHeaderHeight(); + banner.removeEventListener('transitionend', handler); + }); + }, + + init() { + // 初始化 header 高度 + this.updateHeaderHeight(); + + // 监听窗口大小变化 + window.addEventListener('resize', () => this.updateHeaderHeight()); + + // 监听 banner 的变化 + const banner = DOMUtils.find('[data-banner]'); + if (banner) { + window._bannerObserver = new MutationObserver(() => this.updateHeaderHeight()); + window._bannerObserver.observe(banner, { + attributes: true, + childList: true, + subtree: true, + characterData: true + }); + + // 绑定关闭按钮事件 + const closeButton = DOMUtils.find('[data-banner-close]', banner); + if (closeButton) { + closeButton.addEventListener('click', () => this.close(closeButton)); + } + } + } +}; + // Header 组件 const Header = { DarkMode: { @@ -280,6 +365,7 @@ const Header = { init() { this.DarkMode.init(); this.initEventListeners(); + Banner.init(); const observer = new MutationObserver(() => { this.initEventListeners(); @@ -331,12 +417,166 @@ const FontSizeControl = { } }; +const TOC = { + init() { + // 添加样式 + if (!document.getElementById('toc-styles')) { + const style = document.createElement('style'); + style.id = 'toc-styles'; + style.textContent = ` + .toc-link.active-toc-item { + background-color: rgb(239 246 255); + color: rgb(37 99 235); + position: relative; + } + .dark .toc-link.active-toc-item { + background-color: rgba(30 58 138 / 0.2); + color: rgb(96 165 250); + } + .toc-link.active-toc-item::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 16px; + background-color: rgb(37 99 235); + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + } + .dark .toc-link.active-toc-item::before { + background-color: rgb(96 165 250); + } + `; + document.head.appendChild(style); + } + + // 获取所有 TOC 链接 + const tocLinks = document.querySelectorAll('.toc-link'); + + // 处理点击事件 + tocLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + + // 移除所有活跃状态 + tocLinks.forEach(l => l.classList.remove('active-toc-item')); + + // 添加当前项的活跃状态 + link.classList.add('active-toc-item'); + + // 获取目标元素和滚动位置 + const targetId = link.getAttribute('data-slug'); + const target = document.getElementById(targetId); + const header = document.querySelector('header nav'); + const offset = header ? header.offsetHeight + 10 : 80; + const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset; + + // 滚动到目标位置 + window.scrollTo({ + top: targetPosition, + behavior: 'smooth' + }); + + // 如果是移动端,关闭 TOC 面板 + const tocMobile = document.getElementById('toc-mobile'); + if (tocMobile) { + tocMobile.classList.add('translate-y-full'); + } + }); + }); + + // 监听滚动,更新当前活跃项 + const updateActiveItem = () => { + const header = document.querySelector('header nav'); + const headerHeight = header ? header.offsetHeight : 0; + + // 查找所有带 slug 的容器 + const headings = Array.from(document.querySelectorAll('[id].inline-flex')); + + // 找到当前视窗中最靠上的标题 + let current = null; + let minDistance = Infinity; + + headings.forEach(heading => { + const rect = heading.getBoundingClientRect(); + const top = rect.top - headerHeight - 20; + + // 计算到视窗顶部的距离 + const distance = Math.abs(top); + + // 如果元素在视窗上方或接近顶部,且距离比当前最小距离更小 + if (top <= 10 && distance < minDistance) { + minDistance = distance; + current = heading; + } + }); + + // 更新 TOC 活跃状态 + if (current) { + const slug = current.id; + const tocLinks = document.querySelectorAll('.toc-link'); + + tocLinks.forEach(link => { + if (link.getAttribute('data-slug') === slug) { + link.classList.add('active-toc-item'); + + // 确保活跃项在滚动区域内可见 + const nav = link.closest('.overflow-y-auto'); + if (nav) { + const navRect = nav.getBoundingClientRect(); + const linkRect = link.getBoundingClientRect(); + + if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) { + link.scrollIntoView({behavior: 'smooth', block: 'center'}); + } + } + } + else { + link.classList.remove('active-toc-item'); + } + }); + } + }; + + // 使用 IntersectionObserver 来优化滚动监听 + const observer = new IntersectionObserver((entries) => { + entries.forEach(() => { + requestAnimationFrame(updateActiveItem); + }); + }, { + rootMargin: '-20% 0px -80% 0px', + threshold: [0, 1] + }); + + // 观察所有标题元素 + document.querySelectorAll('[id].inline-flex').forEach(heading => { + observer.observe(heading); + }); + + // 仍然保留滚动监听作为备份 + let scrollTimeout; + window.addEventListener('scroll', () => { + if (scrollTimeout) { + window.cancelAnimationFrame(scrollTimeout); + } + scrollTimeout = window.requestAnimationFrame(updateActiveItem); + }, {passive: true}); + + // 初始调用一次 + updateActiveItem(); + } +}; + // 暴露到全局 window.PageForge = { GitHubStats, CodeCopy, Header, - FontSizeControl + FontSizeControl, + Banner, + TOC }; // DOM 加载完成后初始化 @@ -344,6 +584,7 @@ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { Header.init(); FontSizeControl.init(); + TOC.init(); }); } else { diff --git a/templates/includes/header-banner.ejs b/templates/includes/header-banner.ejs index ae5a72d..57fa938 100644 --- a/templates/includes/header-banner.ejs +++ b/templates/includes/header-banner.ejs @@ -5,7 +5,7 @@ <%- locals.siteData.banner.content %> - - +
+ + + <% } %> + + +
+
- <%- include(`${pageData.template || 'content'}`) %> -
-
- - - <% if (locals.pageData?.config?.toc !== false) { %> - <%- include('../includes/toc') %> - <% } %> + <%- include(`${pageData.template || 'content'}`) %> + + + + + <% if (locals.pageData?.config?.toc !== false) { %> + <%- include('../includes/toc') %> + <% } %> +
<% if (locals.pageData?.config?.footer !== false) { %>