save
This commit is contained in:
parent
4b0cee8ddb
commit
b527ad343b
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
|
||||
/* Features Section */
|
||||
.bottomTabsSectionContent {
|
||||
width: 100%;
|
||||
width: calc(100% - 520px);
|
||||
margin: 0 auto;
|
||||
height: 500px;
|
||||
margin-top: 200px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
line-height: 34px;
|
||||
border-bottom: 1px solid #D5D8DC;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.topTabsNavBtn {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue