Files
company-website/src/layouts/Layout.astro
T
2026-05-22 09:30:44 +08:00

411 lines
15 KiB
Plaintext

---
import Footer from '../components/sections/Footer.astro';
import 'font-awesome/css/font-awesome.min.css';
const { title = "浙江贝凡网络科技", activeNav = "home" } = Astro.props;
interface NavChild {
id: string;
label: string;
href: string;
icon: string;
desc?: string;
}
interface NavItem {
id: string;
label: string;
href: string;
icon: string;
children?: NavChild[];
}
const navItems: NavItem[] = [
{ id: 'home', label: '公司介绍', href: '/', icon: 'fa-building' },
{
id: 'products', label: '产品中心', href: '/qazk', icon: 'fa-cubes',
children: [
{ id: 'qazk', label: '工贸企业', href: '/qazk', icon: 'fa-cogs', desc: 'AI + 物联全场景安全管理' },
{ id: 'property', label: '物业楼宇', href: '/property', icon: 'fa-building-o', desc: '智慧社区全域安防一体化' },
{ id: 'elderly', label: '智慧养老', href: '/elderly', icon: 'fa-heartbeat', desc: '适老化主动健康守护体系' },
{ id: 'kitchen', label: '明厨亮灶', href: '/kitchen', icon: 'fa-cutlery', desc: 'AI 视觉后厨食安管控' },
{ id: 'education', label: '智慧学校', href: '/education', icon: 'fa-graduation-cap', desc: '平安校园数智化治理' },
{ id: 'logistics', label: '仓储物流', href: '/logistics', icon: 'fa-truck', desc: '仓配园区全闭环安全管控' },
{ id: 'construction', label: '智慧工地', href: '/construction', icon: 'fa-industry', desc: '工地七牌一图数智管控' },
{ id: 'chemical', label: '危险化学品', href: '/chemical', icon: 'fa-flask', desc: '五防协同危化智能管控' },
{ id: 'fireworks', label: '烟花爆竹', href: '/fireworks', icon: 'fa-asterisk', desc: '防爆 AI 火工品全链路溯源' },
{ id: 'mining', label: '非煤矿山', href: '/mining', icon: 'fa-cubes', desc: '矿山防爆物联灾害预警' },
]
},
{ id: 'customization', label: '本地化定制', href: '/customization', icon: 'fa-wrench' },
{ id: 'partnership', label: '生态合作', href: '/partnership', icon: 'fa-handshake-o' },
];
function isActive(item: NavItem): boolean {
if (item.id === activeNav) return true;
if (item.children) return item.children.some(child => child.id === activeNav);
return false;
}
function getNavLinkClass(item: NavItem) {
const baseClass = 'nav-link flex items-center gap-2 px-5 py-3 rounded-full text-base font-semibold transition-all duration-300 touch-manipulation select-none';
if (isActive(item)) {
return `${baseClass} bg-primary text-white shadow-md`;
}
return `${baseClass} text-gray-600`;
}
function getMobileNavLinkClass(item: NavItem) {
const baseClass = 'flex flex-col items-center justify-center py-2.5 px-3 min-w-[5.5rem] rounded-xl transition-all duration-300 touch-manipulation select-none';
if (isActive(item)) {
return `${baseClass} bg-primary text-white`;
}
return `${baseClass} text-gray-600`;
}
---
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body class="font-inter text-dark bg-slate-50">
<header id="navbar" class="fixed w-full top-0 z-50 transition-all duration-300 bg-white/95 backdrop-blur-md shadow-lg border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 lg:px-0 py-3 flex items-center justify-between hidden lg:flex">
<a href="/" class="flex items-center space-x-3">
<img src="/img/logo.png" alt="浙江贝凡 Logo" class="h-8 w-auto">
</a>
<nav class="hidden lg:flex items-center gap-3">
{
navItems.map(item => item.children ? (
<div class="relative" data-dropdown>
<button type="button" class={`${getNavLinkClass(item)} cursor-pointer`}
aria-haspopup="true" aria-expanded="false" data-dropdown-trigger>
<i class={`fa ${item.icon}`}></i>
<span>{item.label}</span>
<i class="fa fa-angle-down text-xs ml-0.5 transition-transform duration-200" data-dropdown-chevron></i>
</button>
<div class="absolute top-full left-1/2 -translate-x-1/2 pt-3 opacity-0 invisible transition-all duration-200 z-50"
data-dropdown-panel role="menu">
<div class="bg-white/95 backdrop-blur-xl rounded-3xl shadow-2xl shadow-slate-900/10 ring-1 ring-slate-200/70 p-3 w-[720px]">
<div class="grid grid-cols-2 gap-2">
{item.children.map(child => (
<a href={child.href} role="menuitem"
class={`menu-item group relative flex items-center gap-4 px-4 py-3.5 rounded-2xl transition-all duration-200 touch-manipulation select-none ${
child.id === activeNav
? 'bg-gradient-to-br from-primary to-blue-600 text-white shadow-lg shadow-blue-500/30'
: 'text-gray-700'
}`}>
<span class={`menu-item-icon w-16 flex items-center justify-center shrink-0 transition-all duration-200 ${
child.id === activeNav
? 'text-white'
: 'text-primary group-hover:scale-110'
}`}>
<i class={`fa ${child.icon} text-5xl`}></i>
</span>
<span class="flex-1 min-w-0">
<span class="block text-base font-extrabold leading-tight tracking-tight">{child.label}</span>
{child.desc && (
<span class={`block text-xs font-medium mt-1 leading-snug truncate ${
child.id === activeNav ? 'text-blue-50/90' : 'text-gray-500'
}`}>{child.desc}</span>
)}
</span>
</a>
))}
</div>
</div>
</div>
</div>
) : (
<a href={item.href} class={getNavLinkClass(item)}>
<i class={`fa ${item.icon}`}></i>
<span>{item.label}</span>
</a>
))
}
</nav>
<a href="/#contact-info" class="hidden lg:flex items-center gap-2 bg-primary hover:bg-blue-600 text-white px-5 py-2.5 rounded-full font-bold transition-all duration-300 shadow-lg hover:shadow-xl">
<i class="fa fa-phone"></i>
<span>立即咨询</span>
</a>
</div>
<!-- Mobile Header - First Row -->
<div id="mobile-header-row" class="lg:hidden flex items-center justify-between px-4 py-3 bg-white border-b border-gray-100">
<a href="/" class="flex items-center space-x-2">
<img src="/img/logo.png" alt="浙江贝凡 Logo" class="h-6 w-auto">
</a>
</div>
</header>
<!-- Mobile Navigation Row - Separate from header -->
<div id="mobile-nav-container" class="lg:hidden fixed left-0 right-0 z-40 bg-gray-50 border-b border-gray-200 transition-all duration-300" style="top: 60px;">
<div class="flex items-center justify-between px-2 py-2">
<div class="flex items-center gap-1 overflow-x-auto no-scrollbar">
{
navItems.map(item => item.children ? (
<button type="button"
class={getMobileNavLinkClass(item)}
aria-haspopup="true" aria-expanded="false"
data-mobile-dropdown-trigger>
<i class={`fa ${item.icon} text-xl mb-1`}></i>
<span class="text-sm font-semibold whitespace-nowrap">{item.label}</span>
</button>
) : (
<a href={item.href} class={getMobileNavLinkClass(item)}>
<i class={`fa ${item.icon} text-xl mb-1`}></i>
<span class="text-sm font-semibold whitespace-nowrap">{item.label}</span>
</a>
))
}
</div>
<a href="/#contact-info" class="flex items-center justify-center py-2.5 px-3.5 bg-primary text-white rounded-xl min-w-[3.25rem] ml-1.5">
<i class="fa fa-phone text-xl"></i>
</a>
</div>
</div>
<!-- Mobile Product Panel -->
<div id="mobile-product-panel"
class="lg:hidden fixed left-0 right-0 z-30 bg-white shadow-xl border-b border-gray-200 transform -translate-y-full opacity-0 pointer-events-none transition-all duration-300"
style="top: 108px;" role="menu">
<div class="max-w-lg mx-auto px-4 py-4 grid grid-cols-2 gap-3">
{navItems.find(item => item.children)?.children?.map(child => (
<a href={child.href} role="menuitem"
class={`flex flex-col items-center gap-2 py-4 px-2 rounded-xl transition-colors touch-manipulation select-none ${
child.id === activeNav
? 'bg-primary text-white'
: 'bg-primary/5 text-gray-700'
}`}>
<i class={`fa ${child.icon} text-2xl ${child.id === activeNav ? 'text-white' : 'text-primary'}`}></i>
<span class={`text-sm font-semibold leading-tight text-center whitespace-nowrap ${child.id === activeNav ? 'text-white' : 'text-gray-700'}`}>{child.label}</span>
</a>
))}
</div>
</div>
<!-- Mobile Product Backdrop -->
<div id="mobile-product-backdrop"
class="lg:hidden fixed inset-0 z-20 bg-black/20 opacity-0 pointer-events-none transition-opacity duration-300"></div>
<!-- Spacer for mobile to prevent content overlap (header ~3.5rem + nav ~4rem) -->
<div class="lg:hidden h-[7.5rem] transition-all duration-300"></div>
<slot />
<Footer />
<button id="back-to-top" class="fixed bottom-8 right-8 w-12 h-12 bg-primary rounded-full flex items-center justify-center text-white shadow-lg opacity-0 invisible transition-all duration-300 hover:bg-primary/90 z-50">
<i class="fa fa-arrow-up"></i>
</button>
<script is:inline src="/js/script.js"></script>
<script>
// Mobile navigation scroll behavior
(function() {
var navContainer = document.getElementById('mobile-nav-container');
var headerRow = document.getElementById('mobile-header-row');
var spacer = document.querySelector('.lg\\:hidden.h-\\[7\\.5rem\\]');
var lastScrollTop = 0;
var scrollThreshold = 50;
// Set correct top position based on actual header height
function updateNavPosition() {
if (headerRow && navContainer && window.innerWidth < 1024) {
var headerHeight = headerRow.getBoundingClientRect().height;
navContainer.style.top = headerHeight + 'px';
}
}
// Update on load and resize
updateNavPosition();
window.addEventListener('resize', updateNavPosition);
window.addEventListener('load', updateNavPosition);
window.addEventListener('scroll', function() {
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// Only apply on mobile
if (window.innerWidth >= 1024) return;
if (scrollTop > lastScrollTop && scrollTop > scrollThreshold) {
// Scrolling down - hide second row
if (navContainer) {
navContainer.style.transform = 'translateY(-100%)';
navContainer.style.opacity = '0';
}
// Reduce spacer height
if (spacer) {
(spacer as HTMLElement).style.height = '3.5rem';
}
} else {
// Scrolling up - show second row
if (navContainer) {
navContainer.style.transform = 'translateY(0)';
navContainer.style.opacity = '1';
}
// Restore spacer height
if (spacer) {
(spacer as HTMLElement).style.height = '7.5rem';
}
}
lastScrollTop = scrollTop;
}, { passive: true });
})();
// Desktop dropdown hover + keyboard navigation
(function() {
var dropdown = document.querySelector('[data-dropdown]');
if (!dropdown) return;
var trigger = dropdown.querySelector('[data-dropdown-trigger]') as HTMLElement;
var panel = dropdown.querySelector('[data-dropdown-panel]') as HTMLElement;
var chevron = dropdown.querySelector('[data-dropdown-chevron]') as HTMLElement;
var menuItems = panel.querySelectorAll('[role="menuitem"]') as NodeListOf<HTMLElement>;
function openPanel() {
trigger.setAttribute('aria-expanded', 'true');
panel.classList.remove('opacity-0', 'invisible');
panel.classList.add('opacity-100', 'visible');
if (chevron) chevron.style.transform = 'rotate(180deg)';
}
function closePanel() {
trigger.setAttribute('aria-expanded', 'false');
panel.classList.add('opacity-0', 'invisible');
panel.classList.remove('opacity-100', 'visible');
if (chevron) chevron.style.transform = '';
}
// Click to toggle
trigger.addEventListener('click', function(e: Event) {
e.preventDefault();
if (trigger.getAttribute('aria-expanded') === 'true') {
closePanel();
} else {
openPanel();
}
});
// Keyboard
trigger.addEventListener('keydown', function(e: KeyboardEvent) {
if (e.key === 'ArrowDown') {
e.preventDefault();
openPanel();
menuItems[0]?.focus();
}
if (e.key === 'Escape') {
closePanel();
trigger.focus();
}
});
panel.addEventListener('keydown', function(e: KeyboardEvent) {
var items = Array.from(menuItems);
var idx = items.indexOf(document.activeElement as HTMLElement);
if (e.key === 'ArrowDown') {
e.preventDefault();
items[(idx + 1) % items.length].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
items[(idx - 1 + items.length) % items.length].focus();
} else if (e.key === 'Escape') {
closePanel();
trigger.focus();
}
});
document.addEventListener('click', function(e) {
if (!(dropdown as HTMLElement).contains(e.target as Node)) {
closePanel();
}
});
})();
// Mobile product dropdown
(function() {
var trigger = document.querySelector('[data-mobile-dropdown-trigger]') as HTMLElement;
var panel = document.getElementById('mobile-product-panel');
var backdrop = document.getElementById('mobile-product-backdrop');
if (!trigger || !panel) return;
var isOpen = false;
function updatePanelPosition() {
var navContainer = document.getElementById('mobile-nav-container');
if (navContainer && panel) {
var rect = navContainer.getBoundingClientRect();
panel.style.top = (rect.bottom) + 'px';
}
}
function openPanel() {
isOpen = true;
updatePanelPosition();
trigger.setAttribute('aria-expanded', 'true');
panel!.classList.remove('-translate-y-full', 'opacity-0', 'pointer-events-none');
panel!.classList.add('translate-y-0', 'opacity-100', 'pointer-events-auto');
if (backdrop) {
backdrop.classList.remove('opacity-0', 'pointer-events-none');
backdrop.classList.add('opacity-100', 'pointer-events-auto');
}
}
function closePanel() {
isOpen = false;
trigger.setAttribute('aria-expanded', 'false');
panel!.classList.add('-translate-y-full', 'opacity-0', 'pointer-events-none');
panel!.classList.remove('translate-y-0', 'opacity-100', 'pointer-events-auto');
if (backdrop) {
backdrop.classList.add('opacity-0', 'pointer-events-none');
backdrop.classList.remove('opacity-100', 'pointer-events-auto');
}
}
trigger.addEventListener('click', function(e) {
e.preventDefault();
isOpen ? closePanel() : openPanel();
});
if (backdrop) {
backdrop.addEventListener('click', closePanel);
}
window.addEventListener('scroll', function() {
if (isOpen && window.innerWidth < 1024) closePanel();
}, { passive: true });
})();
</script>
<style>
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hover effects only on real pointer devices (avoid sticky hover on iPad/touch) */
@media (hover: hover) and (pointer: fine) {
.nav-link.text-gray-600:hover {
color: var(--color-primary, #165DFF);
background-color: rgb(22 93 255 / 0.1);
}
.menu-item.text-gray-700:hover {
background-color: rgb(22 93 255 / 0.06);
}
}
/* Active tap feedback for touch / iPad */
.nav-link:active,
.menu-item:active {
transform: scale(0.97);
}
</style>
</body>
</html>