You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
296 lines
16 KiB
296 lines
16 KiB
'use client';
|
|
import React, { useState } from "react";
|
|
import Image from "next/image";
|
|
import { SimpleCarousel } from "./SimpleCarousel";
|
|
import type { HeroData } from "../types";
|
|
|
|
interface HomeHeroCarouselProps {
|
|
data: HeroData;
|
|
}
|
|
|
|
export function HomeHeroCarousel({ data }: HomeHeroCarouselProps) {
|
|
const { eyebrow, title, subtitle, carousel } = data;
|
|
// 跟踪第一张图片是否已加载完成
|
|
const [isFirstImageLoaded, setIsFirstImageLoaded] = useState(false);
|
|
|
|
return (
|
|
<section className="relative overflow-hidden bg-[#f5f7fb] pb-14 pt-10 text-[#0f1f39] md:pb-20 md:pt-16">
|
|
<div className="absolute inset-0">
|
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-[320px] bg-gradient-to-b from-white via-[#f5f7fb] to-transparent opacity-60" />
|
|
<div className="pointer-events-none absolute left-[-120px] top-[-140px] h-[360px] w-[360px] rounded-full bg-[radial-gradient(circle,rgba(17,138,244,0.14)_0%,rgba(17,138,244,0)_70%)] blur-3xl opacity-70" />
|
|
</div>
|
|
|
|
<div className="relative mx-auto flex w-full max-w-6xl flex-col items-center gap-6 px-4 text-center md:px-6">
|
|
<div className="max-w-3xl space-y-3">
|
|
{eyebrow && (
|
|
<p className="text-xs font-semibold uppercase tracking-[0.46em] text-[#118af4]">
|
|
{eyebrow}
|
|
</p>
|
|
)}
|
|
<h1 className="text-3xl font-semibold leading-tight text-[#0f1f39] md:text-[40px]">
|
|
{title}
|
|
</h1>
|
|
<p className="text-sm leading-relaxed text-[#4b5565] md:text-base">
|
|
{subtitle}
|
|
</p>
|
|
</div>
|
|
|
|
<SimpleCarousel
|
|
items={carousel}
|
|
className="mt-4 h-[360px] w-full max-w-5xl rounded-[28px] bg-white/90 shadow-[0_30px_60px_rgba(15,31,57,0.08)] md:h-[450px] lg:h-[500px]"
|
|
isReady={isFirstImageLoaded}
|
|
renderItem={(item, index) => {
|
|
// 处理第一张图片的加载完成事件
|
|
const handleImageLoad = () => {
|
|
if (index === 0) {
|
|
setIsFirstImageLoaded(true);
|
|
}
|
|
};
|
|
// 如果有文字,根据 layout 决定布局方式
|
|
if (item.text && item.text.length > 0) {
|
|
// 上下布局(上面文字,下面图片)
|
|
if (item.layout === "vertical" && item.imageBottom) {
|
|
return (
|
|
<div className="flex h-full w-full flex-col overflow-hidden rounded-[28px] border border-[rgba(17,138,244,0.12)] bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff]">
|
|
{/* 上面:文字内容 */}
|
|
<div className="flex flex-1 flex-col justify-center bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff] p-5 text-[#0f1f39] md:p-6 lg:p-8">
|
|
{item.title && (
|
|
<h3 className="mb-2 text-base font-semibold leading-snug text-[#0f1f39] md:mb-3 md:text-lg lg:text-xl break-words">
|
|
{item.title}
|
|
</h3>
|
|
)}
|
|
<div className="space-y-2 text-xs leading-relaxed text-[#1f2937] md:space-y-2.5 md:text-sm md:leading-relaxed lg:text-base">
|
|
{item.text.map((paragraph, index) => {
|
|
// 如果有高亮关键词,渲染带高亮的段落
|
|
if (item.highlights && item.highlights.length > 0) {
|
|
let parts: (string | JSX.Element)[] = [paragraph];
|
|
item.highlights.forEach((highlight, highlightIndex) => {
|
|
const newParts: (string | JSX.Element)[] = [];
|
|
parts.forEach((part) => {
|
|
if (typeof part === 'string') {
|
|
const regex = new RegExp(`(${highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
const matches = part.split(regex);
|
|
matches.forEach((match, matchIndex) => {
|
|
if (matchIndex % 2 === 1) {
|
|
newParts.push(
|
|
<strong key={`p${index}-h${highlightIndex}-m${matchIndex}`} className="text-[#118af4]">
|
|
{match}
|
|
</strong>
|
|
);
|
|
} else if (match) {
|
|
newParts.push(match);
|
|
}
|
|
});
|
|
} else {
|
|
newParts.push(part);
|
|
}
|
|
});
|
|
parts = newParts;
|
|
});
|
|
return (
|
|
<p key={index}>
|
|
{parts}
|
|
</p>
|
|
);
|
|
}
|
|
return <p key={index}>{paragraph}</p>;
|
|
})}
|
|
{/* 统计信息框 */}
|
|
{item.stats && (
|
|
<div className="mt-3 md:mt-4 p-3 md:p-4 rounded-lg bg-gradient-to-br from-[#e8f4fd] to-[#dbeafe] border border-[rgba(17,138,244,0.15)]">
|
|
{item.highlights && item.highlights.length > 0 ? (() => {
|
|
let parts: (string | JSX.Element)[] = [item.stats];
|
|
item.highlights.forEach((highlight, highlightIndex) => {
|
|
const newParts: (string | JSX.Element)[] = [];
|
|
parts.forEach((part) => {
|
|
if (typeof part === 'string') {
|
|
const regex = new RegExp(`(${highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
const matches = part.split(regex);
|
|
matches.forEach((match, matchIndex) => {
|
|
if (matchIndex % 2 === 1) {
|
|
newParts.push(
|
|
<strong key={`stats-h${highlightIndex}-m${matchIndex}`} className="text-[#0f1f39]">
|
|
{match}
|
|
</strong>
|
|
);
|
|
} else if (match) {
|
|
newParts.push(match);
|
|
}
|
|
});
|
|
} else {
|
|
newParts.push(part);
|
|
}
|
|
});
|
|
parts = newParts;
|
|
});
|
|
return <p className="text-xs md:text-sm text-[#1f2937] leading-relaxed">{parts}</p>;
|
|
})() : (
|
|
<p className="text-xs md:text-sm text-[#1f2937] leading-relaxed">{item.stats}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{/* 下面:图片 */}
|
|
<div className="relative flex h-[200px] md:h-[250px] w-full items-center justify-center overflow-hidden bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff]">
|
|
<Image
|
|
src={item.imageBottom}
|
|
alt={item.alt}
|
|
fill
|
|
sizes="100vw"
|
|
className="object-contain"
|
|
priority={item.id === "hero-5"}
|
|
onLoad={index === 0 ? handleImageLoad : undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
// 左右布局(左边文字,右边图片)
|
|
return (
|
|
<div className="flex h-full w-full flex-col overflow-hidden rounded-[28px] border border-[rgba(17,138,244,0.12)] bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff] md:flex-row">
|
|
{/* 左边:文字内容 */}
|
|
<div className="flex flex-1 flex-col justify-center bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff] p-5 text-[#0f1f39] md:p-6 lg:p-8">
|
|
{item.title && (
|
|
<h3 className="mb-2 text-base font-semibold leading-snug text-[#0f1f39] md:mb-3 md:text-lg lg:text-xl break-words">
|
|
{item.title}
|
|
</h3>
|
|
)}
|
|
<div className="space-y-2 text-xs leading-relaxed text-[#1f2937] md:space-y-2.5 md:text-sm md:leading-relaxed lg:text-base">
|
|
{item.text.map((paragraph, index) => {
|
|
// 如果有高亮关键词,渲染带高亮的段落
|
|
if (item.highlights && item.highlights.length > 0) {
|
|
let parts: (string | JSX.Element)[] = [paragraph];
|
|
item.highlights.forEach((highlight, highlightIndex) => {
|
|
const newParts: (string | JSX.Element)[] = [];
|
|
parts.forEach((part) => {
|
|
if (typeof part === 'string') {
|
|
const regex = new RegExp(`(${highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
const matches = part.split(regex);
|
|
matches.forEach((match, matchIndex) => {
|
|
if (matchIndex % 2 === 1) {
|
|
newParts.push(
|
|
<strong key={`p${index}-h${highlightIndex}-m${matchIndex}`} className="text-[#118af4]">
|
|
{match}
|
|
</strong>
|
|
);
|
|
} else if (match) {
|
|
newParts.push(match);
|
|
}
|
|
});
|
|
} else {
|
|
newParts.push(part);
|
|
}
|
|
});
|
|
parts = newParts;
|
|
});
|
|
return (
|
|
<p key={index}>
|
|
{parts}
|
|
</p>
|
|
);
|
|
}
|
|
return <p key={index}>{paragraph}</p>;
|
|
})}
|
|
{/* 统计信息框 */}
|
|
{item.stats && (
|
|
<div className="mt-3 md:mt-4 p-3 md:p-4 rounded-lg bg-gradient-to-br from-[#e8f4fd] to-[#dbeafe] border border-[rgba(17,138,244,0.15)]">
|
|
{item.highlights && item.highlights.length > 0 ? (() => {
|
|
let parts: (string | JSX.Element)[] = [item.stats];
|
|
item.highlights.forEach((highlight) => {
|
|
const newParts: (string | JSX.Element)[] = [];
|
|
parts.forEach((part) => {
|
|
if (typeof part === 'string') {
|
|
const regex = new RegExp(`(${highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
const matches = part.split(regex);
|
|
matches.forEach((match, matchIndex) => {
|
|
if (matchIndex % 2 === 1) {
|
|
newParts.push(
|
|
<strong key={`stats-${matchIndex}`} className="text-[#0f1f39]">
|
|
{match}
|
|
</strong>
|
|
);
|
|
} else if (match) {
|
|
newParts.push(match);
|
|
}
|
|
});
|
|
} else {
|
|
newParts.push(part);
|
|
}
|
|
});
|
|
parts = newParts;
|
|
});
|
|
return <p className="text-xs md:text-sm text-[#1f2937] leading-relaxed">{parts}</p>;
|
|
})() : (
|
|
<p className="text-xs md:text-sm text-[#1f2937] leading-relaxed">{item.stats}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{/* 右边:图片和 KPI 指标 */}
|
|
<div className="flex h-full w-full min-h-0 flex-col items-center justify-between bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff] p-4 md:w-1/2 md:p-6">
|
|
{/* 图片区域 */}
|
|
<div className="relative flex flex-1 w-full max-w-[400px] items-center justify-center min-h-0 mb-3 md:mb-4">
|
|
<div className="h-full w-full flex items-center justify-center bg-gradient-to-br from-[#f0f9ff] to-[#e4f2ff] rounded-lg relative min-h-0">
|
|
<Image
|
|
src={item.src}
|
|
alt={item.alt}
|
|
fill
|
|
sizes="(max-width: 768px) 100vw, 400px"
|
|
className="object-contain"
|
|
style={{
|
|
mixBlendMode: 'multiply',
|
|
filter: 'contrast(1.1) brightness(1.02)'
|
|
}}
|
|
priority={item.id === "hero-1"}
|
|
onLoad={index === 0 ? handleImageLoad : undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* KPI 指标卡片 */}
|
|
{item.kpis && item.kpis.length > 0 && (
|
|
<div className="flex w-full max-w-[400px] gap-2 md:gap-3 flex-shrink-0">
|
|
{item.kpis.map((kpi, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex-1 rounded-lg bg-gradient-to-br from-[#e8f4fd] to-[#dbeafe] border border-[rgba(17,138,244,0.15)] p-3 md:p-4 text-center shadow-[0_2px_8px_rgba(17,138,244,0.08)]"
|
|
>
|
|
<div className="text-lg md:text-xl lg:text-2xl font-bold text-[#0f1f39] mb-1">
|
|
{kpi.value}
|
|
</div>
|
|
<div className="text-xs md:text-sm text-[#4b5565] leading-tight">
|
|
{kpi.label}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
// 没有文字,使用原来的全屏图片布局
|
|
return (
|
|
<div className="relative flex h-full w-full items-center justify-center overflow-hidden rounded-[28px] border border-[rgba(17,138,244,0.12)] bg-gradient-to-br from-white via-[#f7faff] to-[#eaf3ff]">
|
|
<Image
|
|
src={item.src}
|
|
alt={item.alt}
|
|
fill
|
|
sizes="100vw"
|
|
className="object-cover"
|
|
priority={item.id === "hero-2"}
|
|
onLoad={index === 0 ? handleImageLoad : undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
}}
|
|
keyExtractor={(item) => item.id}
|
|
interval={6000}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
|
|
|