重构layout重构header和菜单

This commit is contained in:
jingrow 2025-06-24 11:41:49 +08:00
parent 7cb5ccec85
commit 260bce7b28
8 changed files with 302 additions and 333 deletions

View File

@ -1,205 +1,14 @@
"use client";
import { useEffect, useRef } from "react";
import "../public/assets/style.css";
import "photoswipe/dist/photoswipe.css";
import iTooltip from "itooltip";
import { usePathname } from "next/navigation";
import scrollQue from "../utils/scrollCue.min.js";
import Context from "@/context/Context";
import ProgressWrap from "@/components/common/ProgressWrap";
import initPlayer from "@/utils/initPlayer";
import SearchModal from "@/components/modals/SearchModal";
import InfoModal from "@/components/modals/InfoModal";
import Header from "@/components/headers/Header";
import Footer from "@/components/footers/Footer";
import ClientAppEffects from "@/components/common/ClientAppEffects";
export default function RootLayout({ children }) {
const pathname = usePathname();
useEffect(() => {
if (typeof window !== "undefined") {
import("bootstrap/dist/js/bootstrap.esm").then((module) => {
// Module is imported, you can access any exported functionality if
});
}
}, []);
// const rellaxRef = useRef(null);
useEffect(() => {
if (typeof window !== "undefined") {
scrollQue().init();
window.dispatchEvent(new Event("scroll"));
}
return () => {
// rellaxRef.current?.destroy();
};
}, [pathname]);
useEffect(() => {
initPlayer();
const overlayElements = document.querySelectorAll(
".overlay > a, .overlay > span"
);
overlayElements.forEach((element) => {
const overlayBg = document.createElement("span");
overlayBg.className = "bg";
element.appendChild(overlayBg);
});
}, [pathname]);
useEffect(() => {
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]'
);
const popoverTriggerList = document.querySelectorAll(
'[data-bs-toggle="popover"]'
);
if (tooltipTriggerList.length > 0 || popoverTriggerList.length > 0) {
import("bootstrap/dist/js/bootstrap.bundle.min").then((bootstrap) => {
// Initialize tooltips
const tooltipList = Array.from(tooltipTriggerList).map(
(tooltipTriggerEl) => {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: "hover",
});
}
);
// Initialize popovers
const popoverList = Array.from(popoverTriggerList).map(
(popoverTriggerEl) => {
return new bootstrap.Popover(popoverTriggerEl);
}
);
// Cleanup tooltips and popovers on component unmount
return () => {
tooltipList.forEach((tooltip) => tooltip.dispose());
popoverList.forEach((popover) => popover.dispose());
};
});
}
}, [pathname]);
useEffect(() => {
const handleSticky = () => {
const navbar = document.querySelector(".navbar");
if (navbar) {
if (window.scrollY > 120) {
navbar.classList.add("fixed");
navbar.classList.add("navbar-clone");
if (
navbar.classList.contains("transparent") &&
navbar.classList.contains("navbar-dark")
) {
navbar.classList.remove("navbar-dark");
navbar.classList.add("navbar-light");
navbar.classList.add("navbar-dark-removed");
}
} else {
navbar.classList.remove("fixed");
navbar.classList.remove("navbar-clone");
if (
navbar.classList.contains("transparent") &&
navbar.classList.contains("navbar-dark-removed")
) {
navbar.classList.add("navbar-dark");
navbar.classList.remove("navbar-light");
navbar.classList.remove("navbar-dark-removed");
}
}
if (window.scrollY > 300) {
navbar.classList.add("navbar-stick");
} else {
navbar.classList.remove("navbar-stick");
}
}
};
window.addEventListener("scroll", handleSticky);
}, []);
useEffect(() => {
// Close any open modal
const bootstrap = require("bootstrap"); // dynamically import bootstrap
const modalElements = document.querySelectorAll(".modal.show");
modalElements.forEach((modal) => {
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide();
}
});
// Close any open offcanvas
const offcanvasElements = document.querySelectorAll(".offcanvas.show");
offcanvasElements.forEach((offcanvas) => {
const offcanvasInstance = bootstrap.Offcanvas.getInstance(offcanvas);
if (offcanvasInstance) {
offcanvasInstance.hide();
}
});
// Select all elements with the class 'offcanvas-backdrop'
const backdrops = document.querySelectorAll(".offcanvas-backdrop");
// Check if any backdrop elements exist and remove them
backdrops?.forEach((backdrop) => {
backdrop?.remove();
});
}, [pathname]); // Runs every time the route changes
useEffect(() => {
// Assuming iTooltip is globally available
var tooltip = new iTooltip(".itooltip");
tooltip.init({
className: "itooltip-inner",
indentX: 15,
indentY: 15,
positionX: "right",
positionY: "bottom",
});
}, [pathname]);
useEffect(() => {
setTimeout(() => {
import("bootstrap").then(({ Offcanvas }) => {
const navbar = document.querySelector(".navbar");
if (!navbar) return;
const navOffCanvasBtn = document.querySelectorAll(".offcanvas-nav-btn");
const navOffCanvas = document.querySelector(
".navbar:not(.navbar-clone) .offcanvas-nav"
);
if (!navOffCanvas) return;
const bsOffCanvas = new Offcanvas(navOffCanvas, { scroll: true });
const scrollLink = document.querySelectorAll(
".onepage .navbar li a.scroll"
);
const searchOffcanvas = document.getElementById("offcanvas-search");
const handleNavClick = () => bsOffCanvas.show();
const handleScrollClick = () => bsOffCanvas.hide();
navOffCanvasBtn.forEach((e) =>
e.addEventListener("click", handleNavClick)
);
scrollLink.forEach((e) =>
e.addEventListener("click", handleScrollClick)
);
if (searchOffcanvas) {
searchOffcanvas.addEventListener("shown.bs.offcanvas", () => {
document.getElementById("search-form")?.focus();
});
}
return () => {
navOffCanvasBtn.forEach((e) =>
e.removeEventListener("click", handleNavClick)
);
scrollLink.forEach((e) =>
e.removeEventListener("click", handleScrollClick)
);
};
});
});
}, [pathname]);
return (
<html lang="en">
<head>
@ -213,7 +22,6 @@ export default function RootLayout({ children }) {
rel="stylesheet"
/>
</head>
<body>
<Header />
<Context>
@ -223,6 +31,7 @@ export default function RootLayout({ children }) {
<ProgressWrap />
</Context>
<Footer />
<ClientAppEffects />
</body>
</html>
);

View File

@ -0,0 +1,194 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import iTooltip from "itooltip";
import scrollQue from "@/utils/scrollCue.min.js";
import initPlayer from "@/utils/initPlayer";
export default function ClientAppEffects() {
const pathname = usePathname();
useEffect(() => {
if (typeof window !== "undefined") {
import("bootstrap/dist/js/bootstrap.esm");
}
}, []);
useEffect(() => {
if (typeof window !== "undefined") {
scrollQue().init();
window.dispatchEvent(new Event("scroll"));
}
}, [pathname]);
useEffect(() => {
initPlayer();
const overlayElements = document.querySelectorAll(
".overlay > a, .overlay > span"
);
overlayElements.forEach((element) => {
const overlayBg = document.createElement("span");
overlayBg.className = "bg";
element.appendChild(overlayBg);
});
}, [pathname]);
useEffect(() => {
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]'
);
const popoverTriggerList = document.querySelectorAll(
'[data-bs-toggle="popover"]'
);
if (tooltipTriggerList.length > 0 || popoverTriggerList.length > 0) {
import("bootstrap/dist/js/bootstrap.bundle.min").then((bootstrap) => {
// Initialize tooltips
const tooltipList = Array.from(tooltipTriggerList).map(
(tooltipTriggerEl) => {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: "hover",
});
}
);
// Initialize popovers
const popoverList = Array.from(popoverTriggerList).map(
(popoverTriggerEl) => {
return new bootstrap.Popover(popoverTriggerEl);
}
);
// Cleanup tooltips and popovers on component unmount
return () => {
tooltipList.forEach((tooltip) => tooltip.dispose());
popoverList.forEach((popover) => popover.dispose());
};
});
}
}, [pathname]);
useEffect(() => {
const handleSticky = () => {
const navbar = document.querySelector(".navbar");
if (navbar) {
if (window.scrollY > 120) {
navbar.classList.add("fixed");
navbar.classList.add("navbar-clone");
if (
navbar.classList.contains("transparent") &&
navbar.classList.contains("navbar-dark")
) {
navbar.classList.remove("navbar-dark");
navbar.classList.add("navbar-light");
navbar.classList.add("navbar-dark-removed");
}
} else {
navbar.classList.remove("fixed");
navbar.classList.remove("navbar-clone");
if (
navbar.classList.contains("transparent") &&
navbar.classList.contains("navbar-dark-removed")
) {
navbar.classList.add("navbar-dark");
navbar.classList.remove("navbar-light");
navbar.classList.remove("navbar-dark-removed");
}
}
if (window.scrollY > 300) {
navbar.classList.add("navbar-stick");
} else {
navbar.classList.remove("navbar-stick");
}
}
};
window.addEventListener("scroll", handleSticky);
return () => window.removeEventListener("scroll", handleSticky);
}, []);
useEffect(() => {
import("bootstrap").then((bootstrap) => {
const modalElements = document.querySelectorAll(".modal.show");
modalElements.forEach((modal) => {
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide();
}
});
// Close any open offcanvas
const offcanvasElements = document.querySelectorAll(".offcanvas.show");
offcanvasElements.forEach((offcanvas) => {
const offcanvasInstance = bootstrap.Offcanvas.getInstance(offcanvas);
if (offcanvasInstance) {
offcanvasInstance.hide();
}
});
// Select all elements with the class 'offcanvas-backdrop'
const backdrops = document.querySelectorAll(".offcanvas-backdrop");
backdrops?.forEach((backdrop) => {
backdrop?.remove();
});
});
}, [pathname]);
useEffect(() => {
var tooltip = new iTooltip(".itooltip");
tooltip.init({
className: "itooltip-inner",
indentX: 15,
indentY: 15,
positionX: "right",
positionY: "bottom",
});
}, [pathname]);
useEffect(() => {
setTimeout(() => {
import("bootstrap").then(({ Offcanvas }) => {
const navbar = document.querySelector(".navbar");
if (!navbar) return;
const navOffCanvasBtn = document.querySelectorAll(".offcanvas-nav-btn");
const navOffCanvas = document.querySelector(
".navbar:not(.navbar-clone) .offcanvas-nav"
);
if (!navOffCanvas) return;
const bsOffCanvas = new Offcanvas(navOffCanvas, { scroll: true });
const scrollLink = document.querySelectorAll(
".onepage .navbar li a.scroll"
);
const searchOffcanvas = document.getElementById("offcanvas-search");
const handleNavClick = () => bsOffCanvas.show();
const handleScrollClick = () => bsOffCanvas.hide();
navOffCanvasBtn.forEach((e) =>
e.addEventListener("click", handleNavClick)
);
scrollLink.forEach((e) =>
e.addEventListener("click", handleScrollClick)
);
if (searchOffcanvas) {
searchOffcanvas.addEventListener("shown.bs.offcanvas", () => {
document.getElementById("search-form")?.focus();
});
}
return () => {
navOffCanvasBtn.forEach((e) =>
e.removeEventListener("click", handleNavClick)
);
scrollLink.forEach((e) =>
e.removeEventListener("click", handleScrollClick)
);
};
});
});
}, [pathname]);
return null;
}

View File

@ -1,3 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import axios from "axios";
import ReactDOM from "react-dom";

View File

@ -1,5 +1,4 @@
'use client';
import React, { useEffect, useState } from "react";
import React from "react";
import Nav from "./Nav";
import Link from "next/link";
import Image from "next/image";
@ -7,13 +6,8 @@ import LoginButton from "./LoginButton";
import SocialLinks from "../contact/SocialLinks";
import { getSiteSettings } from "@/utils/siteSettings";
export default function Header15() {
const [siteSettings, setSiteSettings] = useState({});
useEffect(() => {
getSiteSettings().then(setSiteSettings);
}, []);
export default async function Header() {
const siteSettings = await getSiteSettings();
const siteName = siteSettings.site_name || "Jsite";
const mobile = siteSettings.mobile;
const tel = siteSettings.tel;

View File

@ -1,131 +0,0 @@
"use client";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import {
blockItems,
blogItems,
demos,
docsPages,
otherPages,
projectPages,
} from "@/data/menu";
import { usePathname } from "next/navigation";
// !text-[var(--current-color)]
export default function Nav({ color = "#fab758" }) {
const [menu, setMenu] = useState([]);
const pathname = usePathname();
useEffect(() => {
// Dynamically import Bootstrap
import("bootstrap").then((Bootstrap) => {
const CLASS_NAME = "has-child-dropdown-show";
// Save the original toggle function
const originalToggle = Bootstrap.Dropdown.prototype.toggle;
// Override the toggle function
Bootstrap.Dropdown.prototype.toggle = function () {
// Remove the CLASS_NAME from all dropdowns
document.querySelectorAll("." + CLASS_NAME).forEach(function (e) {
e.classList.remove(CLASS_NAME);
});
// Traverse up through the closest dropdown parents
let dd = this._element
.closest(".dropdown")
.parentNode.closest(".dropdown");
for (; dd && dd !== document; dd = dd.parentNode.closest(".dropdown")) {
dd.classList.add(CLASS_NAME);
}
// Call the original toggle function
return originalToggle.call(this);
};
// Add event listeners for hide.bs.dropdown to remove the CLASS_NAME
document.querySelectorAll(".dropdown").forEach(function (dd) {
dd.addEventListener("hide.bs.dropdown", function (e) {
if (this.classList.contains(CLASS_NAME)) {
this.classList.remove(CLASS_NAME);
e.preventDefault();
}
e.stopPropagation();
});
});
});
//
fetch("/api/get-menu")
.then((res) => res.json())
.then((data) => setMenu(data.menu || []));
// Optional cleanup function for any potential side effects
return () => {
// Cleanup code here (if needed)
};
}, []);
// position
const filterHeaderMenu = (items) => {
return items.filter(item => item.position === 'Header');
};
//
const renderMenu = (items, level = 0) => {
// position
if (level === 0) items = filterHeaderMenu(items);
return items.map((item) => {
const hasChildren = item.children && item.children.length > 0;
const hasHref = !!item.href && item.href !== "#";
if (hasChildren) {
return (
<li key={item.id} className={`nav-item dropdown${level > 0 ? " dropdown-submenu dropend" : ""}`}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{/* 如果有href左侧为可点击跳转始终用当前item的href */}
{hasHref && (
<Link
className={`${level > 0 ? "dropdown-item" : "nav-link"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)] ${pathname === item.href ? "!text-[var(--current-color)]" : ""}`}
href={item.href}
style={{ paddingRight: 0 }}
>
{item.label}
</Link>
)}
{/* 右侧小三角,负责展开下拉 */}
<a
className={`${hasHref ? "dropdown-toggle" : (level > 0 ? "dropdown-item" : "nav-link") + " dropdown-toggle"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)] ${!hasHref && pathname === item.href ? "!text-[var(--current-color)]" : ""}`}
href="#"
data-bs-toggle="dropdown"
aria-expanded="false"
style={hasHref ? { paddingLeft: 8, minWidth: 24 } : {}}
>
{hasHref ? <span className="sr-only">展开</span> : item.label}
</a>
</div>
<ul className={`dropdown-menu${level > 0 ? " submenu" : ""}`}>
{renderMenu(item.children, level + 1)}
</ul>
</li>
);
} else {
return (
<li key={item.id} className="nav-item">
<Link
className={`${level > 0 ? "dropdown-item" : "nav-link"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)] ${pathname === item.href ? "!text-[var(--current-color)]" : ""}`}
href={item.href || "#"}
>
{item.label}
</Link>
</li>
);
}
});
};
return (
<ul className="navbar-nav" style={{ "--current-color": color }}>
{renderMenu(menu)}
</ul>
);
}

View File

@ -0,0 +1,62 @@
import Link from "next/link";
import React from "react";
export default function NavUI({ menu = [], color = "#fab758" }) {
const filterHeaderMenu = (items) => {
return items.filter(item => item.position === 'Header');
};
const renderMenu = (items, level = 0) => {
if (level === 0) items = filterHeaderMenu(items);
return items.map((item) => {
const hasChildren = item.children && item.children.length > 0;
const hasHref = !!item.href && item.href !== "#";
if (hasChildren) {
return (
<li key={item.id} className={`nav-item dropdown${level > 0 ? " dropdown-submenu dropend" : ""}`}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{hasHref && (
<Link
className={`${level > 0 ? "dropdown-item" : "nav-link"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)]`}
href={item.href}
style={{ paddingRight: 0 }}
>
{item.label}
</Link>
)}
<a
className={`${hasHref ? "dropdown-toggle" : (level > 0 ? "dropdown-item" : "nav-link") + " dropdown-toggle"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)]`}
href="#"
data-bs-toggle="dropdown"
aria-expanded="false"
style={hasHref ? { paddingLeft: 8, minWidth: 24 } : {}}
>
{hasHref ? <span className="sr-only">展开</span> : item.label}
</a>
</div>
<ul className={`dropdown-menu${level > 0 ? " submenu" : ""}`}>
{renderMenu(item.children, level + 1)}
</ul>
</li>
);
} else {
return (
<li key={item.id} className="nav-item">
<Link
className={`${level > 0 ? "dropdown-item" : "nav-link"} !text-[.7rem] !tracking-[normal] hover:!text-[var(--current-color)] after:!text-[var(--current-color)]`}
href={item.href || "#"}
>
{item.label}
</Link>
</li>
);
}
});
};
return (
<ul className="navbar-nav" style={{ "--current-color": color }}>
{renderMenu(menu)}
</ul>
);
}

View File

@ -0,0 +1,7 @@
import { getMenuData } from '@/utils/data';
import NavUI from './NavUI';
export default async function Nav(props) {
const { menu } = await getMenuData();
return <NavUI menu={menu} {...props} />;
}

View File

@ -237,4 +237,36 @@ export async function fetchCategoryData({ pagetype, name }) {
console.error(`Error fetching category data:`, error.message);
return { data: null };
}
}
// 通用获取菜单数据函数
export async function getMenuData() {
try {
const response = await axios.get(
`${JINGROW_SERVER_URL}/api/method/jsite.api.v1.get_menu`
);
const items = response.data.message?.data || [];
// 递归组装菜单树
function buildMenuTree(items, parent = null, parentPath = "") {
return items
.filter(item => (item.parent_jsite_menu === parent || (!item.parent_jsite_menu && !parent)))
.map(item => {
let currentPath = parentPath ? `${parentPath}/${item.slug.replace(/^\//, "")}` : item.slug;
if (!currentPath.startsWith('/')) currentPath = '/' + currentPath;
return {
id: item.name,
label: item.title,
href: currentPath,
position: item.position,
order: item.order,
icon: item.icon,
children: buildMenuTree(items, item.name, currentPath),
};
});
}
const menuTree = buildMenuTree(items);
return { menu: menuTree };
} catch (error) {
return { error: error.message, detail: error?.response?.data || null };
}
}