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 { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, EffectFade } from "swiper/modules";
import type { Swiper as SwiperType } from "swiper";
import "swiper/css";
import "swiper/css/effect-fade";
import styles from "./Banner.module.css";
import { useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useStore } from "@/store";
import type { BannerConfig, NavItem, NavChild } from "@/type";
import ScrollReveal from "@/components/ScrollReveal";
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";
type Props = {
@ -101,7 +140,23 @@ export default function Banner({
</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 (
<section
className={styles.hero}
@ -120,18 +175,24 @@ export default function Banner({
allowTouchMove={false}
slidesPerView={1}
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) => (
<SwiperSlide key={i}>
{/* 图片后缀 */}
{videoExtension.includes(img.split(".").pop() ?? "") ? (
<video
{isVideoUrl(img) ? (
<BannerSlideVideo
src={img}
active={carouselRealIndex === i}
onEnded={handleVideoEnded}
className={styles.bgVideo}
autoPlay
muted
loop
playsInline
/>
) : (
<div
@ -141,11 +202,20 @@ export default function Banner({
}}
/>
)}
{/* 视频 */}
</SwiperSlide>
))}
</Swiper>
)}
{!isCarousel && isVideoUrl(images[0]) && (
<video
src={images[0]}
className={styles.bgVideo}
autoPlay
muted
loop
playsInline
/>
)}
<div className={styles.heroOverlay} />
{heroContent}
</section>

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import styles from './index.module.css';
import TopTabs from './TopTabs';
import { useLocation } from 'react-router-dom';
import ScrollReveal from '@/components/ScrollReveal';
type Data = {
@ -55,22 +56,35 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
<>
<div className={styles.topTabsContentLeft}>
<div className={styles.topTabsContentLeftHead}>
{
<ScrollReveal preset="slideRight" delay={0.3} >
{
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 : ''}`}>
<ScrollReveal preset="slideUp" delay={0.5} duration={1.1}>
<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>
</ScrollReveal>
</div>
</div>
<ScrollReveal preset="slideUp" delay={0.9} duration={1.1}>
<p className={styles.topTabsContentLeftDesc}>
{data.tabItems[activeIndex].contentText ?? data.tabItems[activeIndex].content}
</p>
</ScrollReveal>
</div>
<div className={styles.topTabsContentRight}>
<ScrollReveal preset="slideLeft" delay={0.5} >
<img src={data.tabItems[activeIndex].sideImage} alt="side-image" />
</ScrollReveal>
</div>
</>
)

View File

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

View File

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

View File

@ -70,7 +70,7 @@
.twoColImage {
min-width: 0;
background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%);
background: #f0f2f4;
display: flex;
align-items: center;
justify-content: center;
@ -266,6 +266,7 @@
background: #14355C;
border-color: #14355C;
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 TopTabs from "@/components/layout/TopTabsSection/TopTabs";
import SectionTitle from "@/components/layout/SectionTitle";
import ScrollReveal from "@/components/ScrollReveal";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
@ -16,11 +17,11 @@ function PlaceholderImage() {
export default function BusinessCommercialGroup() {
const appConfig = useStore((s) => s.appConfig);
const { viewDetail="查看详情" } = appConfig?.__global__?.others
const { viewDetail = "查看详情" } = appConfig?.__global__?.others
const data = appConfig?.business?.commercialGroup;
const section3Data = data?.section3Data;
const section4Data = data?.section4Data;
const [in77ImgError, setIn77ImgError] = useState(false);
const [activeTabIndex, setActiveTabIndex] = 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} />
<div className={styles.twoColSectionContent}>
<div className={styles.twoColImage}>
{in77ImgError ? (
<PlaceholderImage />
) : (
{(
<ScrollReveal preset="slideRight" duration={1}>
<img
src={section2Data.tabItems[activeTabIndex]?.sideImage as string}
alt=""
onError={() => setIn77ImgError(true)}
style={{ width: "100%", height: "800px" }}
/>
</ScrollReveal>
)}
</div>
<div className={styles.twoColText}>
<ScrollReveal preset="slideUp" delay={0.5} >
<p className={styles.twoColDesc} dangerouslySetInnerHTML={{ __html: section2Data.tabItems[activeTabIndex]?.content ?? "" }}></p>
</ScrollReveal>
<Link
to={section2Data.tabItems[activeTabIndex]?.path ?? "#"}
className={styles.btnPrimary}
@ -148,8 +150,12 @@ export default function BusinessCommercialGroup() {
</div>
<SectionTitle color="#fff" title={section3Data.title} subcontent={section3Data.content} />
<div className={styles.featuresHeroContent}>
<ScrollReveal preset="slideUp" >
<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 ref={featuresHeroTabsRef} className={styles.featuresHeroTabs}>
<div
@ -184,11 +190,13 @@ export default function BusinessCommercialGroup() {
}}
>
<div className={styles.propertyServicesContent}>
<div className={styles.propertyServicesTitle}></div>
<p className={styles.propertyServicesSubtitle}></p>
<Link to="/property-service" className={styles.propertyServicesBtn}>
<div className={styles.propertyServicesTitle}>{section4Data?.title}</div>
<p className={styles.propertyServicesSubtitle}>{section4Data?.content}</p>
<ScrollReveal preset="slideUp" className={styles.propertyServicesBtn}>
<Link to={section4Data?.path ?? "#"} >
{viewDetail}
</Link>
</ScrollReveal>
</div>
</section>
</div>