137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
import { Link, useLocation } from "react-router-dom";
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
import { Autoplay, EffectFade } from "swiper/modules";
|
|
import "swiper/css";
|
|
import "swiper/css/effect-fade";
|
|
import styles from "./Banner.module.css";
|
|
import { useMemo } from "react";
|
|
import { useStore } from "@/store";
|
|
|
|
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
|
|
|
|
|
|
export type BannerConfig = {
|
|
title?: string;
|
|
content?: string;
|
|
subtitle?: string;
|
|
largeContent?: string;
|
|
titleSize?: "large" | "medium" | string;
|
|
showBreadcrumb?: boolean;
|
|
backgroundImage?: string | string[];
|
|
};
|
|
|
|
type Props = {
|
|
title: string;
|
|
subtitle?: string;
|
|
desc?: string;
|
|
content?: string;
|
|
largedesc?: string;
|
|
showBreadcrumb?: boolean;
|
|
titleSize?: "large" | "medium" | string;
|
|
backgroundImage: string | string[];
|
|
};
|
|
|
|
export default function Banner({
|
|
title,
|
|
subtitle,
|
|
desc,
|
|
content,
|
|
largedesc,
|
|
showBreadcrumb,
|
|
titleSize = "large",
|
|
backgroundImage,
|
|
}: Props) {
|
|
const appConfig = useStore((s) => s.appConfig);
|
|
const header = appConfig?.header;
|
|
const navItems = header?.navItems ?? [];
|
|
|
|
const location = useLocation();
|
|
const images = Array.isArray(backgroundImage) ? backgroundImage : [backgroundImage];
|
|
const isCarousel = images.length > 1;
|
|
const descText = desc ?? content;
|
|
|
|
const breadcrumbItems = useMemo(() => {
|
|
const segments = location.pathname.split("/").filter((s) => s !== "");
|
|
if (segments.length === 0) {
|
|
return [{ label: "首页", to: "/" }];
|
|
}
|
|
const paths: string[] = [];
|
|
for (let i = 0; i < segments.length; i++) {
|
|
paths.push((paths[i - 1] ?? "") + "/" + segments[i]);
|
|
}
|
|
const getLabelByPath = (path: string): string => {
|
|
if (path === "/") return navItems.find((n) => n.index)?.label ?? "首页";
|
|
const top = navItems.find((n) => n.path === path);
|
|
if (top) return top.label;
|
|
for (const item of navItems) {
|
|
const child = item.children?.find((c) => c.path === path);
|
|
if (child) return child.label;
|
|
}
|
|
const last = path.split("/").pop() ?? path;
|
|
return last;
|
|
};
|
|
const items = paths.map((path) => ({
|
|
label: getLabelByPath(path),
|
|
to: path,
|
|
}));
|
|
items.unshift({
|
|
label: navItems.find((n) => n.index)?.label ?? "首页",
|
|
to: "/",
|
|
});
|
|
return items;
|
|
}, [location.pathname, navItems]);
|
|
|
|
const heroContent = (
|
|
<div className={styles.heroContent} style={{ gap: "30px" }}>
|
|
<h1 className={`${styles.heroTitle} ${titleSize === "medium" ? styles.heroTitleMedium : ""}`}>{title}</h1>
|
|
{subtitle && <h2 className={styles.heroSubtitle}>{subtitle}</h2>}
|
|
{descText && <p className={styles.heroDesc}>{descText}</p>}
|
|
{largedesc && <p className={styles.heroLargeDesc}>{largedesc}</p>}
|
|
<div className={styles.breadcrumb}>
|
|
{showBreadcrumb &&
|
|
(breadcrumbItems ?? []).map((item, i) => (
|
|
<span key={i}>
|
|
{i > 0 && <span>{" > "}</span>}
|
|
{item.to ? <Link to={item.to}>{item.label}</Link> : <span>{item.label}</span>}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
return (
|
|
<section
|
|
className={styles.hero}
|
|
style={
|
|
isCarousel
|
|
? undefined
|
|
: { backgroundImage: `url(${images[0]}), ${FALLBACK_GRADIENT}` }
|
|
}
|
|
>
|
|
{isCarousel && (
|
|
<Swiper
|
|
className={styles.bgSwiper}
|
|
modules={[Autoplay, EffectFade]}
|
|
effect="slide"
|
|
autoplay={{ delay: 3000, disableOnInteraction: false }}
|
|
allowTouchMove={false}
|
|
slidesPerView={1}
|
|
loop={true}
|
|
>
|
|
{images.map((img, i) => (
|
|
<SwiperSlide key={i}>
|
|
<div
|
|
className={styles.bgSlide}
|
|
style={{
|
|
backgroundImage: `url(${img}), ${FALLBACK_GRADIENT}`,
|
|
}}
|
|
/>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
)}
|
|
<div className={styles.heroOverlay} />
|
|
{heroContent}
|
|
</section>
|
|
);
|
|
}
|