This commit is contained in:
zhangjianjun 2026-03-24 14:41:10 +08:00
parent 4b0cee8ddb
commit b527ad343b
9 changed files with 153 additions and 45 deletions

View File

@ -1,16 +1,55 @@
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, EffectFade } from "swiper/modules"; import { Autoplay, EffectFade } from "swiper/modules";
import type { Swiper as SwiperType } from "swiper";
import "swiper/css"; import "swiper/css";
import "swiper/css/effect-fade"; import "swiper/css/effect-fade";
import styles from "./Banner.module.css"; import styles from "./Banner.module.css";
import { useMemo } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import type { BannerConfig, NavItem, NavChild } from "@/type"; import type { BannerConfig, NavItem, NavChild } from "@/type";
import ScrollReveal from "@/components/ScrollReveal"; import ScrollReveal from "@/components/ScrollReveal";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)"; const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
const VIDEO_EXT = ["mp4", "webm", "ogg"];
function mediaExtension(url: string) {
const path = url.split("?")[0] ?? "";
return path.split(".").pop()?.toLowerCase() ?? "";
}
function isVideoUrl(url: string) {
return VIDEO_EXT.includes(mediaExtension(url));
}
function BannerSlideVideo({
src,
active,
onEnded,
className,
}: {
src: string;
active: boolean;
onEnded: () => void;
className: string;
}) {
const ref = useRef<HTMLVideoElement>(null);
useEffect(() => {
const v = ref.current;
if (!v) return;
if (active) {
v.currentTime = 0;
void v.play().catch(() => {});
} else {
v.pause();
}
}, [active, src]);
return (
<video ref={ref} src={src} className={className} muted playsInline onEnded={onEnded} />
);
}
export type { BannerConfig } from "@/type"; export type { BannerConfig } from "@/type";
type Props = { type Props = {
@ -101,7 +140,23 @@ export default function Banner({
</div> </div>
); );
const videoExtension = ["mp4", "webm", "ogg"]; const swiperRef = useRef<SwiperType | null>(null);
const [carouselRealIndex, setCarouselRealIndex] = useState(0);
const syncAutoplayForMedia = useCallback((swiper: SwiperType, urls: string[]) => {
const idx = swiper.realIndex;
const url = urls[idx];
if (url && isVideoUrl(url)) {
swiper.autoplay?.stop();
} else {
swiper.autoplay?.start();
}
}, []);
const handleVideoEnded = useCallback(() => {
swiperRef.current?.slideNext();
}, []);
return ( return (
<section <section
className={styles.hero} className={styles.hero}
@ -120,18 +175,24 @@ export default function Banner({
allowTouchMove={false} allowTouchMove={false}
slidesPerView={1} slidesPerView={1}
loop={true} loop={true}
onSwiper={(swiper: SwiperType) => {
swiperRef.current = swiper;
setCarouselRealIndex(swiper.realIndex);
syncAutoplayForMedia(swiper, images);
}}
onSlideChange={(swiper: SwiperType) => {
setCarouselRealIndex(swiper.realIndex);
syncAutoplayForMedia(swiper, images);
}}
> >
{images.map((img, i) => ( {images.map((img, i) => (
<SwiperSlide key={i}> <SwiperSlide key={i}>
{/* 图片后缀 */} {isVideoUrl(img) ? (
{videoExtension.includes(img.split(".").pop() ?? "") ? ( <BannerSlideVideo
<video
src={img} src={img}
active={carouselRealIndex === i}
onEnded={handleVideoEnded}
className={styles.bgVideo} className={styles.bgVideo}
autoPlay
muted
loop
playsInline
/> />
) : ( ) : (
<div <div
@ -141,11 +202,20 @@ export default function Banner({
}} }}
/> />
)} )}
{/* 视频 */}
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
)} )}
{!isCarousel && isVideoUrl(images[0]) && (
<video
src={images[0]}
className={styles.bgVideo}
autoPlay
muted
loop
playsInline
/>
)}
<div className={styles.heroOverlay} /> <div className={styles.heroOverlay} />
{heroContent} {heroContent}
</section> </section>

View File

@ -12,7 +12,8 @@
/* Features Section */ /* Features Section */
.bottomTabsSectionContent { .bottomTabsSectionContent {
width: 100%; width: calc(100% - 520px);
margin: 0 auto;
height: 500px; height: 500px;
margin-top: 200px; margin-top: 200px;
display: flex; display: flex;

View File

@ -6,6 +6,7 @@ import SectionTitle from "../SectionTitle";
import styles from "./index.module.css"; import styles from "./index.module.css";
import { RightOutlined, LeftOutlined } from "@ant-design/icons"; import { RightOutlined, LeftOutlined } from "@ant-design/icons";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import ScrollReveal from "@/components/ScrollReveal";
type Data = { type Data = {
title: string; title: string;
cardItems: { cardItems: {
@ -60,8 +61,9 @@ export default function SwiperCardSection({ data }: { data: Data }) {
className={styles.swiperCardList} className={styles.swiperCardList}
onSlideChange={onSwiperChange} onSlideChange={onSwiperChange}
> >
{data.cardItems.map((item) => ( {data.cardItems.map((item, index) => (
<SwiperSlide key={item.title}> <SwiperSlide key={item.title}>
<ScrollReveal preset="slideUp" amount={0.2} delay={index > 2 ? 0 : index * 0.5}>
<div className={styles.swiperCardItem}> <div className={styles.swiperCardItem}>
<img src={item.backgroundImage ?? item.image ?? ""} alt={item.title} /> <img src={item.backgroundImage ?? item.image ?? ""} alt={item.title} />
<div className={styles.swiperMask}></div> <div className={styles.swiperMask}></div>
@ -72,6 +74,7 @@ export default function SwiperCardSection({ data }: { data: Data }) {
<p className={styles.swiperDesc}>{item.content ?? item.desc}</p> <p className={styles.swiperDesc}>{item.content ?? item.desc}</p>
</div> </div>
</div> </div>
</ScrollReveal>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>

View File

@ -15,7 +15,6 @@
line-height: 34px; line-height: 34px;
border-bottom: 1px solid #D5D8DC; border-bottom: 1px solid #D5D8DC;
width: 100%; width: 100%;
overflow-y: hidden;
} }
.topTabsNavBtn { .topTabsNavBtn {

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import styles from './index.module.css'; import styles from './index.module.css';
import TopTabs from './TopTabs'; import TopTabs from './TopTabs';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import ScrollReveal from '@/components/ScrollReveal';
type Data = { type Data = {
@ -56,21 +57,34 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
<div className={styles.topTabsContentLeft}> <div className={styles.topTabsContentLeft}>
<div className={styles.topTabsContentLeftHead}> <div className={styles.topTabsContentLeftHead}>
{ {
data.tabItems[activeIndex].icon && ( <ScrollReveal preset="slideRight" delay={0.3} >
<img src={data.tabItems[activeIndex].icon} alt="" style={{ width: '100px', height: '100px' }} /> {
) data.tabItems[activeIndex].icon && (
<img src={data.tabItems[activeIndex].icon} alt="" style={{ width: '100px', height: '100px' }} />
)
}
</ScrollReveal>
} }
<div className={`${styles.topTabsContentLeftTitle} ${data.titleDirection === 'column' ? styles.columnCenter : ''}`}> <div className={`${styles.topTabsContentLeftTitle} ${data.titleDirection === 'column' ? styles.columnCenter : ''}`}>
<ScrollReveal preset="slideUp" delay={0.5} duration={1.1}>
<div className={styles.topTabsContentLeftTitleMain}>{data.tabItems[activeIndex].contentTitle}</div> <div className={styles.topTabsContentLeftTitleMain}>{data.tabItems[activeIndex].contentTitle}</div>
</ScrollReveal>
<ScrollReveal preset="slideUp" delay={0.7} duration={1.1}>
<div className={styles.topTabsContentLeftTitleSub}>{data.tabItems[activeIndex].contentSubtitle}</div> <div className={styles.topTabsContentLeftTitleSub}>{data.tabItems[activeIndex].contentSubtitle}</div>
</ScrollReveal>
</div> </div>
</div> </div>
<p className={styles.topTabsContentLeftDesc}> <ScrollReveal preset="slideUp" delay={0.9} duration={1.1}>
{data.tabItems[activeIndex].contentText ?? data.tabItems[activeIndex].content} <p className={styles.topTabsContentLeftDesc}>
</p> {data.tabItems[activeIndex].contentText ?? data.tabItems[activeIndex].content}
</p>
</ScrollReveal>
</div> </div>
<div className={styles.topTabsContentRight}> <div className={styles.topTabsContentRight}>
<ScrollReveal preset="slideLeft" delay={0.5} >
<img src={data.tabItems[activeIndex].sideImage} alt="side-image" /> <img src={data.tabItems[activeIndex].sideImage} alt="side-image" />
</ScrollReveal>
</div> </div>
</> </>
) )

View File

@ -6,6 +6,7 @@ import Section from "@/components/layout/Section";
import SineWaveTimeline from "@/components/SineWaveTimeline"; import SineWaveTimeline from "@/components/SineWaveTimeline";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Lenis from "lenis" import Lenis from "lenis"
import ScrollReveal from "@/components/ScrollReveal";
export default function AboutFounder() { export default function AboutFounder() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
@ -32,6 +33,7 @@ export default function AboutFounder() {
<ParagraphSection data={section1Data}> <ParagraphSection data={section1Data}>
<div className={styles.images}> <div className={styles.images}>
{section1Data.items?.map((item: { title: string; content?: string; backgroundImage?: string }, index: number) => ( {section1Data.items?.map((item: { title: string; content?: string; backgroundImage?: string }, index: number) => (
<ScrollReveal preset="slideUp" amount={0.2} delay={index * 0.5} key={item.title}>
<div className={styles.imageItem} key={item.title}> <div className={styles.imageItem} key={item.title}>
<img src={item.backgroundImage} alt={item.title} /> <img src={item.backgroundImage} alt={item.title} />
<div className={styles.imageMask} /> <div className={styles.imageMask} />
@ -42,6 +44,7 @@ export default function AboutFounder() {
<div className={styles.imageOverlayDesc} dangerouslySetInnerHTML={{ __html: item.content ?? "" }}></div> <div className={styles.imageOverlayDesc} dangerouslySetInnerHTML={{ __html: item.content ?? "" }}></div>
</div> </div>
</div> </div>
</ScrollReveal>
))} ))}
</div> </div>
</ParagraphSection> </ParagraphSection>
@ -61,10 +64,14 @@ export default function AboutFounder() {
<p>{section2Data.subtitle}</p> <p>{section2Data.subtitle}</p>
</div> </div>
<div className={styles.founderPhoto}> <div className={styles.founderPhoto}>
<ScrollReveal preset="slideRight" amount={0.2} delay={0} duration={1}>
<img src={section2Data.sideImage} alt="个人照" /> <img src={section2Data.sideImage} alt="个人照" />
</ScrollReveal>
<ScrollReveal preset="slideUp" amount={0.2} delay={0.9} duration={1}>
<div className={styles.founderPhotoContent}> <div className={styles.founderPhotoContent}>
<p dangerouslySetInnerHTML={{ __html: section2Data.content ?? "" }} /> <p dangerouslySetInnerHTML={{ __html: section2Data.content ?? "" }} />
</div> </div>
</ScrollReveal>
</div> </div>
</section> </section>
)} )}
@ -75,9 +82,11 @@ export default function AboutFounder() {
<div className={styles.section3Content}> <div className={styles.section3Content}>
{ {
section3Data?.items.map((item: { title?: string }, index: number) => ( section3Data?.items.map((item: { title?: string }, index: number) => (
<ScrollReveal preset="slideUp" amount={0.2} delay={index * 0.5} key={item.title}>
<div key={index} className={styles.section3Item}> <div key={index} className={styles.section3Item}>
{item.title ? <li>{item.title}</li> : null} {item.title ? <li>{item.title}</li> : null}
</div> </div>
</ScrollReveal>
)) ))
} }
</div> </div>

View File

@ -4,6 +4,7 @@ import styles from "./History.module.css";
import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react"; import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import appApi from "@/api/app"; import appApi from "@/api/app";
import ScrollReveal from "@/components/ScrollReveal";
type TimelineItem = { year: number; content: string, lang: string }; type TimelineItem = { year: number; content: string, lang: string };
export default function AboutHistory() { export default function AboutHistory() {
@ -29,7 +30,7 @@ export default function AboutHistory() {
// 接口数据 // 接口数据
useEffect(() => { useEffect(() => {
appApi.getProcessList().then((res) => { appApi.getProcessList().then((res) => {
const items = res.data.items.map((item:any) => { const items = res.data.items.map((item: any) => {
return { return {
year: item.title, year: item.title,
content: item.content, content: item.content,
@ -141,13 +142,15 @@ function HistoryTimeline({ data, selectedYear, onYearChange }: HistoryTimelinePr
<div className={styles.timelineLine} /> <div className={styles.timelineLine} />
<div className={styles.timelineContent}> <div className={styles.timelineContent}>
{data.map((item, index) => ( {data.map((item, index) => (
<TimelineItemRow <ScrollReveal preset="slideUp" delay={index * 0.1} key={item.year}>
key={item.year} <TimelineItemRow
item={item} key={item.year}
index={index} item={item}
isSelected={selectedYear === item.year} index={index}
onMouseEnter={() => onYearChange(item.year)} isSelected={selectedYear === item.year}
/> onMouseEnter={() => onYearChange(item.year)}
/>
</ScrollReveal>
))} ))}
</div> </div>
</div> </div>

View File

@ -70,7 +70,7 @@
.twoColImage { .twoColImage {
min-width: 0; min-width: 0;
background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); background: #f0f2f4;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -266,6 +266,7 @@
background: #14355C; background: #14355C;
border-color: #14355C; border-color: #14355C;
color: #FFFFFF; color: #FFFFFF;
border: 1px solid #FFFFFF;
} }

View File

@ -7,6 +7,7 @@ import ParagraphSection from "@/components/layout/ParagraphSection";
import Section from "@/components/layout/Section"; import Section from "@/components/layout/Section";
import TopTabs from "@/components/layout/TopTabsSection/TopTabs"; import TopTabs from "@/components/layout/TopTabsSection/TopTabs";
import SectionTitle from "@/components/layout/SectionTitle"; import SectionTitle from "@/components/layout/SectionTitle";
import ScrollReveal from "@/components/ScrollReveal";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)"; const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
@ -16,11 +17,11 @@ function PlaceholderImage() {
export default function BusinessCommercialGroup() { export default function BusinessCommercialGroup() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const { viewDetail="查看详情" } = appConfig?.__global__?.others const { viewDetail = "查看详情" } = appConfig?.__global__?.others
const data = appConfig?.business?.commercialGroup; const data = appConfig?.business?.commercialGroup;
const section3Data = data?.section3Data; const section3Data = data?.section3Data;
const section4Data = data?.section4Data;
const [in77ImgError, setIn77ImgError] = useState(false);
const [activeTabIndex, setActiveTabIndex] = useState(0); const [activeTabIndex, setActiveTabIndex] = useState(0);
const [activeFeaturesTabIndex, setActiveFeaturesTabIndex] = useState(0); const [activeFeaturesTabIndex, setActiveFeaturesTabIndex] = useState(0);
@ -102,19 +103,20 @@ export default function BusinessCommercialGroup() {
<TopTabs className={styles.twoColSectionTabs} data={section2Data} activeIndex={activeTabIndex} setActiveIndex={setActiveTabIndex} /> <TopTabs className={styles.twoColSectionTabs} data={section2Data} activeIndex={activeTabIndex} setActiveIndex={setActiveTabIndex} />
<div className={styles.twoColSectionContent}> <div className={styles.twoColSectionContent}>
<div className={styles.twoColImage}> <div className={styles.twoColImage}>
{in77ImgError ? ( {(
<PlaceholderImage /> <ScrollReveal preset="slideRight" duration={1}>
) : ( <img
<img src={section2Data.tabItems[activeTabIndex]?.sideImage as string}
src={section2Data.tabItems[activeTabIndex]?.sideImage as string} alt=""
alt="" style={{ width: "100%", height: "800px" }}
onError={() => setIn77ImgError(true)} />
style={{ width: "100%", height: "800px" }} </ScrollReveal>
/>
)} )}
</div> </div>
<div className={styles.twoColText}> <div className={styles.twoColText}>
<p className={styles.twoColDesc} dangerouslySetInnerHTML={{ __html: section2Data.tabItems[activeTabIndex]?.content ?? "" }}></p> <ScrollReveal preset="slideUp" delay={0.5} >
<p className={styles.twoColDesc} dangerouslySetInnerHTML={{ __html: section2Data.tabItems[activeTabIndex]?.content ?? "" }}></p>
</ScrollReveal>
<Link <Link
to={section2Data.tabItems[activeTabIndex]?.path ?? "#"} to={section2Data.tabItems[activeTabIndex]?.path ?? "#"}
className={styles.btnPrimary} className={styles.btnPrimary}
@ -148,8 +150,12 @@ export default function BusinessCommercialGroup() {
</div> </div>
<SectionTitle color="#fff" title={section3Data.title} subcontent={section3Data.content} /> <SectionTitle color="#fff" title={section3Data.title} subcontent={section3Data.content} />
<div className={styles.featuresHeroContent}> <div className={styles.featuresHeroContent}>
<div className={styles.featuresHeroContentTitle}>{section3Data.tabItems[activeFeaturesTabIndex]?.tabName}</div> <ScrollReveal preset="slideUp" >
<div className={styles.featuresHeroContentDesc}>{section3Data.tabItems[activeFeaturesTabIndex]?.content}</div> <div className={styles.featuresHeroContentTitle}>{section3Data.tabItems[activeFeaturesTabIndex]?.tabName}</div>
</ScrollReveal>
<ScrollReveal preset="slideUp" >
<div className={styles.featuresHeroContentDesc}>{section3Data.tabItems[activeFeaturesTabIndex]?.content}</div>
</ScrollReveal>
</div> </div>
<div ref={featuresHeroTabsRef} className={styles.featuresHeroTabs}> <div ref={featuresHeroTabsRef} className={styles.featuresHeroTabs}>
<div <div
@ -184,11 +190,13 @@ export default function BusinessCommercialGroup() {
}} }}
> >
<div className={styles.propertyServicesContent}> <div className={styles.propertyServicesContent}>
<div className={styles.propertyServicesTitle}></div> <div className={styles.propertyServicesTitle}>{section4Data?.title}</div>
<p className={styles.propertyServicesSubtitle}></p> <p className={styles.propertyServicesSubtitle}>{section4Data?.content}</p>
<Link to="/property-service" className={styles.propertyServicesBtn}> <ScrollReveal preset="slideUp" className={styles.propertyServicesBtn}>
<Link to={section4Data?.path ?? "#"} >
{viewDetail} {viewDetail}
</Link> </Link>
</ScrollReveal>
</div> </div>
</section> </section>
</div> </div>