blog / src /components /layout /Navbar.astro
cacode's picture
Upload 434 files
96dd062 verified
---
import { Picture } from "astro:assets";
import * as path from "node:path";
import type { ImageMetadata } from "astro";
import { Icon } from "astro-icon/components";
import DisplaySettings from "@/components/controls/DisplaySettingsIntegrated.svelte";
import LightDarkSwitch from "@/components/controls/LightDarkSwitch.svelte";
import Search from "@/components/controls/Search.svelte";
import MusicPlayer from "@/components/features/MusicPlayer.astro";
import { backgroundWallpaper, navBarConfig, siteConfig } from "@/config";
import { musicPlayerConfig } from "@/config/musicConfig";
import { LinkPresets } from "@/constants/link-presets";
import I18nKey from "@/i18n/i18nKey";
import { i18n } from "@/i18n/translation";
import { LinkPreset, type NavBarLink } from "@/types/config";
import { getFallbackFormat, getImageFormats } from "@/utils/image-utils";
import { isHomePage } from "@/utils/layout-utils";
import { url } from "@/utils/url-utils";
import DropdownMenu from "./DropdownMenu.astro";
import NavMenuPanel from "./NavMenuPanel.astro";
const className = Astro.props.class;
// 获取导航栏透明模式配置
const navbarTransparentMode =
backgroundWallpaper.mode === "banner"
? backgroundWallpaper.banner?.navbar?.transparentMode || "semi"
: "semi";
// 获取导航栏毛玻璃效果配置
const navbarEnableBlur =
backgroundWallpaper.mode === "banner"
? (backgroundWallpaper.banner?.navbar?.enableBlur ?? true)
: false;
// 获取导航栏模糊度
const navbarBlur =
backgroundWallpaper.mode === "banner"
? (backgroundWallpaper.banner?.navbar?.blur ?? 20)
: 0;
// 获取导航栏标题,如果没有设置则使用 siteConfig.title
const navbarTitle = siteConfig.navbar.title || siteConfig.title;
// 获取导航栏宽度配置
const navbarWidthFull = siteConfig.navbar.widthFull ?? false;
// 检查是否为首页
const isHomePageCheck = isHomePage(Astro.url.pathname);
// 检查是否有任何显示设置可用
const showThemeColor = !siteConfig.themeColor.fixed;
const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
const allowLayoutSwitch = siteConfig.postListLayout.allowSwitch;
const hasDisplaySettings =
showThemeColor || isWallpaperSwitchable || allowLayoutSwitch;
let links: NavBarLink[] = navBarConfig.links.map(
(item: NavBarLink | LinkPreset): NavBarLink => {
if (typeof item === "number") {
return LinkPresets[item];
}
return item;
},
);
// 处理导航栏 Logo 图片
const logoValue = siteConfig.navbar.logo?.value || "";
const isLocalSrcLogo =
siteConfig.navbar.logo?.type === "image" &&
logoValue &&
!logoValue.startsWith("/") &&
!logoValue.startsWith("http");
let logoImg: ImageMetadata | null = null;
if (isLocalSrcLogo) {
const files = import.meta.glob<ImageMetadata>(
"../../**/*.{png,jpg,jpeg,webp,avif,gif,svg}",
{ import: "default" },
);
const normalizedPath = path
.normalize(path.join("../../", logoValue))
.replace(/\\/g, "/");
const file = files[normalizedPath];
if (file) {
logoImg = await file();
}
}
---
<div id="navbar" class="z-50" style={`--navbar-glass-blur: ${navbarBlur}px;`} data-transparent-mode={navbarTransparentMode} data-enable-blur={String(navbarEnableBlur)} data-is-home={isHomePageCheck} data-full-width={navbarWidthFull}>
<div class:list={[
className,
"overflow-visible! h-18 mx-auto flex items-center px-4",
navbarWidthFull ? "" : "justify-between max-w-(--page-width)"
]}>
<a href={url('/')} class="btn-plain scale-animation rounded-lg h-13 px-5 font-bold active:scale-95">
<div class:list={[
"flex flex-row items-center text-md",
siteConfig.navbar.followTheme ? "text-(--primary)" : "dark:text-white text-black"
]}>
{siteConfig.navbar.logo?.type === "icon" ? (
<Icon name={siteConfig.navbar.logo.value || "material-symbols:home-pin-outline"} class="text-[1.75rem] mb-1 mr-2" />
) : siteConfig.navbar.logo?.type === "image" && logoImg ? (
<Picture
src={logoImg}
alt={siteConfig.navbar.logo.alt || siteConfig.title}
class="h-7 w-7 mb-1 mr-2 object-contain"
formats={getImageFormats()}
fallbackFormat={getFallbackFormat()}
widths={[28, 56]}
loading="eager"
fetchpriority="high"
/>
) : siteConfig.navbar.logo?.type === "image" ? (
<img src={url(siteConfig.navbar.logo.value)} alt={siteConfig.navbar.logo.alt || siteConfig.title} class="h-7 w-7 mb-1 mr-2 object-contain" fetchpriority="high" />
) : siteConfig.navbar.logo?.type === "url" ? (
<img src={siteConfig.navbar.logo.value} alt={siteConfig.navbar.logo.alt || siteConfig.title} class="h-7 w-7 mb-1 mr-2 object-contain" fetchpriority="high" />
) : (
<Icon name="material-symbols:home-pin-outline" class="text-[1.75rem] mb-1 mr-2" />
)}
{navbarTitle}
</div>
</a>
<div class="hidden lg:flex items-center space-x-1">
{links.map((l) => {
return <DropdownMenu link={l} />;
})}
</div>
<div class:list={["flex", navbarWidthFull ? "ml-auto" : ""]}>
<!--<SearchPanel client:load>-->
<Search
client:load
/>
{musicPlayerConfig.showInNavbar && (
<button aria-label={i18n(I18nKey.music)} class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="music-player-switch">
<Icon name="material-symbols:music-note-rounded" class="text-[1.25rem]"></Icon>
</button>
)}
{hasDisplaySettings && (
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
</button>
)}
{/* @ts-ignore */}
<LightDarkSwitch client:load></LightDarkSwitch>
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 lg:hidden!" id="nav-menu-switch">
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
</button>
</div>
{musicPlayerConfig.showInNavbar && (
<div id="music-nav-panel" class="float-panel float-panel-closed absolute transition-all right-16 p-2 pt-4.5 w-80 z-50">
<MusicPlayer />
</div>
)}
<NavMenuPanel links={links}></NavMenuPanel>
<DisplaySettings client:load></DisplaySettings>
</div>
</div>
<script>
function loadButtonScript() {
let settingBtn = document.getElementById("display-settings-switch");
if (settingBtn) {
settingBtn.onclick = function () {
let settingPanel = document.getElementById("display-setting");
if (settingPanel) {
settingPanel.classList.toggle("float-panel-closed");
}
};
}
let menuBtn = document.getElementById("nav-menu-switch");
if (menuBtn) {
menuBtn.onclick = function () {
let menuPanel = document.getElementById("nav-menu-panel");
if (menuPanel) {
menuPanel.classList.toggle("float-panel-closed");
}
};
}
let musicBtn = document.getElementById("music-player-switch");
if (musicBtn) {
musicBtn.onclick = function () {
let musicPanel = document.getElementById("music-nav-panel");
if (musicPanel) {
musicPanel.classList.toggle("float-panel-closed");
}
};
}
let themeSwitchBtn = document.getElementById("scheme-switch");
if (themeSwitchBtn) {
themeSwitchBtn.onclick = function () {
let themePanel = document.getElementById("theme-mode-panel");
if (themePanel) {
themePanel.classList.toggle("float-panel-closed");
}
};
}
document.addEventListener("click", function(event) {
const panels = ["display-setting", "nav-menu-panel", "music-nav-panel", "theme-mode-panel"];
const buttons = ["display-settings-switch", "nav-menu-switch", "music-player-switch", "scheme-switch"];
panels.forEach((id, index) => {
const panel = document.getElementById(id);
const button = document.getElementById(buttons[index]);
if (panel && button && !panel.classList.contains("float-panel-closed")) {
if (event.target instanceof Node && !panel.contains(event.target) && !button.contains(event.target)) {
panel.classList.add("float-panel-closed");
}
}
});
});
}
loadButtonScript();
// 为semifull模式添加滚动检测逻辑
function initSemifullScrollDetection() {
const navbar = document.getElementById('navbar');
if (!navbar) return;
const transparentMode = navbar.getAttribute('data-transparent-mode');
if (transparentMode !== 'semifull') return;
const isHomePage = navbar.getAttribute('data-is-home') === 'true';
// 如果不是首页,移除滚动事件监听器并设置为半透明状态
if (!isHomePage) {
// 移除之前的滚动事件监听器(如果存在)
if (window.semifullScrollHandler) {
window.removeEventListener('scroll', window.semifullScrollHandler as () => void);
window.semifullScrollHandler = undefined;
}
// 设置为半透明状态
navbar.classList.add('scrolled');
return;
}
// 移除现有的scrolled类,重置状态
navbar.classList.remove('scrolled');
let ticking = false;
function updateNavbarState() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const threshold = 50; // 滚动阈值,可以根据需要调整
// 使用批量DOM操作优化性能
if (scrollTop > threshold) {
navbar!.classList.add('scrolled');
} else {
navbar!.classList.remove('scrolled');
}
ticking = false;
}
function requestTick() {
if (!ticking) {
requestAnimationFrame(updateNavbarState);
ticking = true;
}
}
// 移除之前的滚动事件监听器(如果存在)
if (window.semifullScrollHandler) {
window.removeEventListener('scroll', window.semifullScrollHandler as () => void);
}
// 保存新的事件处理器引用
window.semifullScrollHandler = requestTick as unknown as (() => void);
// 监听滚动事件
window.addEventListener('scroll', requestTick, { passive: true });
// 初始化状态
updateNavbarState();
}
// 将函数暴露到全局对象,供页面切换时调用
window.initSemifullScrollDetection = initSemifullScrollDetection;
// 页面加载完成后初始化滚动检测
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSemifullScrollDetection);
} else {
initSemifullScrollDetection();
}
</script>
{
import.meta.env.PROD && (
<script is:inline define:vars={{ scriptUrl: url('/pagefind/pagefind.js') }}>
{/* 你的 loadPagefind 函数的完整内容放在这里 */}
async function loadPagefind() {
try {
const response = await fetch(scriptUrl, { method: 'HEAD' });
if (!response.ok) {
throw new Error(`Pagefind script not found: ${response.status}`);
}
const pagefind = await import(scriptUrl);
await pagefind.options({
excerptLength: 20
});
window.pagefind = pagefind;
document.dispatchEvent(new CustomEvent('pagefindready'));
console.log('Pagefind loaded and initialized successfully, event dispatched.');
} catch (error) {
console.error('Failed to load Pagefind:', error);
window.pagefind = {
search: () => Promise.resolve({ results: [] }),
options: () => Promise.resolve(),
};
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
console.log('Pagefind load error, event dispatched.');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadPagefind);
} else {
loadPagefind();
}
</script>
)
}