This commit is contained in:
zhangjianjun 2026-03-24 10:56:19 +08:00
parent 3f5380f9c4
commit fabe5c893f
13 changed files with 196 additions and 47 deletions

View File

@ -45,6 +45,7 @@
"jest": "^27.4.3", "jest": "^27.4.3",
"jest-resolve": "^27.4.2", "jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^1.0.0", "jest-watch-typeahead": "^1.0.0",
"lenis": "^1.3.19",
"mime": "^4.0.7", "mime": "^4.0.7",
"mini-css-extract-plugin": "^2.4.5", "mini-css-extract-plugin": "^2.4.5",
"motion": "^12.23.25", "motion": "^12.23.25",
@ -56,6 +57,7 @@
"postcss-preset-env": "^7.0.1", "postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-activation": "^0.13.4",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@ -79,8 +81,7 @@
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^4.0.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"workbox-webpack-plugin": "^6.4.1", "workbox-webpack-plugin": "^6.4.1",
"zustand": "^5.0.11", "zustand": "^5.0.11"
"react-activation": "^0.13.4"
}, },
"scripts": { "scripts": {
"dev": "node --stack-size=12800 --stack-trace-limit=20 scripts/start.js", "dev": "node --stack-size=12800 --stack-trace-limit=20 scripts/start.js",

View File

@ -2,6 +2,7 @@
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
overflow-y: auto; overflow-y: auto;
cursor: all-scroll;
} }
.content { .content {

View File

@ -19,6 +19,7 @@ export type TimelineItem = {
type Props = { type Props = {
items: TimelineItem[]; items: TimelineItem[];
height?: number; height?: number;
refElement?: React.RefObject<HTMLDivElement | null>;
}; };
function generateSinePath( function generateSinePath(
@ -101,7 +102,7 @@ function computeContentWidth(items: TimelineItem[]): number {
return lastX + 280; return lastX + 280;
} }
export default function SineWaveTimeline({ items, height: propHeight = 400 }: Props) { export default function SineWaveTimeline({ items, height: propHeight = 400, refElement }: Props) {
if (!items?.length) return null; if (!items?.length) return null;
const height = propHeight ?? 400; const height = propHeight ?? 400;
@ -112,7 +113,7 @@ export default function SineWaveTimeline({ items, height: propHeight = 400 }: Pr
const bgPathD = generateCosinePath(contentWidth, height, AMPLITUDE * 0.9); const bgPathD = generateCosinePath(contentWidth, height, AMPLITUDE * 0.9);
return ( return (
<div className={styles.scrollContainer}> <div className={styles.scrollContainer} ref={refElement}>
<div <div
className={styles.content} className={styles.content}
style={{ width: contentWidth, height: `${expandedHeight}px` }} style={{ width: contentWidth, height: `${expandedHeight}px` }}

View File

@ -19,3 +19,22 @@
text-transform: none; text-transform: none;
} }
} }
.tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 40px;
margin-top: 100px;
.tag {
font-family: Source Han Sans, Source Han Sans;
font-weight: 500;
font-size: 24px;
color: #14355C;
line-height: 30px;
padding: 24px 40px;
background: #F0F2F4;
border-radius: 326px 326px 326px 326px;
}
}

View File

@ -6,15 +6,25 @@ type Data = {
content: string; content: string;
statsData?: { num: string; label: string }[]; statsData?: { num: string; label: string }[];
backgroundImage?: string; backgroundImage?: string;
tags?: string[];
} }
export default function ParagraphSection({ data, children }: {data: Data, children?: React.ReactNode}) { export default function ParagraphSection({ data, children }: { data: Data, children?: React.ReactNode }) {
return ( return (
<section className={styles.paragraphSection} style={{ backgroundImage: `url(${data.backgroundImage})` }}> <section className={styles.paragraphSection} style={{ backgroundImage: `url(${data.backgroundImage})` }}>
<div className={`${styles.paragraphSectionContent} normal-p`}> <div className={`${styles.paragraphSectionContent} normal-p`}>
<p><span className={styles.paragraphSectionTitle}>{data.title}</span>{data.content}</p> <p><span className={styles.paragraphSectionTitle}>{data.title} </span>{data.content}</p>
</div> </div>
{data.statsData && <StatsRow data={data.statsData} />} {data.statsData && <StatsRow data={data.statsData} />}
{
data.tags && <div className={styles.tags}>
{
data.tags.map((tag) => (
<div key={tag} className={styles.tag}>{tag}</div>
))
}
</div>
}
{children} {children}
</section> </section>

View File

@ -35,4 +35,5 @@
.statLabel { .statLabel {
font-size: 1rem; font-size: 1rem;
color: var(--stats-label-color); color: var(--stats-label-color);
text-align: center;
} }

View File

@ -1,6 +1,7 @@
import { useRef, useEffect, useState, useCallback } from 'react'; import { useRef, useEffect, useState, useCallback } from 'react';
import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import styles from './index.module.css'; import styles from './index.module.css';
import { useStore } from '@/store';
type Data = { type Data = {
tabItems: { tabItems: {
@ -19,6 +20,8 @@ type Data = {
className?: string; className?: string;
} }
export default function TopTabs({ data, activeIndex, setActiveIndex, className }: { data: Data, activeIndex: number, setActiveIndex: (index: number) => void, className?: string }) { export default function TopTabs({ data, activeIndex, setActiveIndex, className }: { data: Data, activeIndex: number, setActiveIndex: (index: number) => void, className?: string }) {
const store = useStore();
const locale = useStore((state) => state.locale);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
@ -99,9 +102,17 @@ export default function TopTabs({ data, activeIndex, setActiveIndex, className }
}; };
}, [activeIndex, data.tabItems.length, updateIndicatorPosition, updateScrollState]); }, [activeIndex, data.tabItems.length, updateIndicatorPosition, updateScrollState]);
useEffect(() => {
console.log('locale', locale);
updateIndicatorPosition();
}, [locale]);
return ( return (
<div className={className}> <div className={className}>
<div ref={containerRef} className={styles.topTabsTabs}> <div ref={containerRef} className={styles.topTabsTabs}
>
{canScrollLeft && ( {canScrollLeft && (
<button <button
type="button" type="button"

View File

@ -62,6 +62,7 @@
/* 隐藏滚动条 */ /* 隐藏滚动条 */
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
@ -159,3 +160,23 @@
height: 500px; height: 500px;
} }
} }
.topTabsContentItems {
width: 920px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.topTabsContentItem {
list-style: disc;
font-family: Source Han Sans, Source Han Sans;
font-weight: 500;
font-size: 20px;
color: #222222;
line-height: 30px;
text-align: left;
font-style: normal;
text-transform: none;
}

View File

@ -3,6 +3,7 @@ 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';
type Data = { type Data = {
tabItems: { tabItems: {
icon?: string; icon?: string;
@ -14,6 +15,7 @@ type Data = {
/** 以 mockData 为准 */ /** 以 mockData 为准 */
sideImage?: string; sideImage?: string;
path?: string; path?: string;
items?: {label: string}[];
}[] }[]
backgroundImage?: string; backgroundImage?: string;
titleDirection?: 'row' | 'column'; titleDirection?: 'row' | 'column';
@ -29,7 +31,6 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
if (id && data.tabItems) { if (id && data.tabItems) {
setTimeout(() => { setTimeout(() => {
const index = data.tabItems.findIndex((item) => item.tabName === id); const index = data.tabItems.findIndex((item) => item.tabName === id);
console.log('index', index, id, data.tabItems)
if (index !== -1) { if (index !== -1) {
setActiveIndex(index); setActiveIndex(index);
} }
@ -40,6 +41,18 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
<section id={id} className={`${styles.topTabsSection} ${className}`} style={{ backgroundImage: `url(${data.backgroundImage})` }}> <section id={id} className={`${styles.topTabsSection} ${className}`} style={{ backgroundImage: `url(${data.backgroundImage})` }}>
<TopTabs data={data} activeIndex={activeIndex} setActiveIndex={setActiveIndex} /> <TopTabs data={data} activeIndex={activeIndex} setActiveIndex={setActiveIndex} />
<div className={styles.topTabsContent}> <div className={styles.topTabsContent}>
{
data.tabItems[activeIndex]?.items && (data.tabItems[activeIndex]?.items as any).length > 0 ?
<ul className={styles.topTabsContentItems}>
{
(data.tabItems[activeIndex]?.items as any).map((item: {label: string}) => (
<li key={item.label} className={styles.topTabsContentItem}>
<span>{item.label}</span>
</li>
))
}
</ul> : (
<>
<div className={styles.topTabsContentLeft}> <div className={styles.topTabsContentLeft}>
<div className={styles.topTabsContentLeftHead}> <div className={styles.topTabsContentLeftHead}>
{ {
@ -59,6 +72,10 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
<div className={styles.topTabsContentRight}> <div className={styles.topTabsContentRight}>
<img src={data.tabItems[activeIndex].sideImage} alt="side-image" /> <img src={data.tabItems[activeIndex].sideImage} alt="side-image" />
</div> </div>
</>
)
}
</div> </div>
</section> </section>
) )

View File

@ -4,6 +4,8 @@ import ParagraphSection from "@/components/layout/ParagraphSection";
import { useStore } from "@/store"; import { useStore } from "@/store";
import Section from "@/components/layout/Section"; import Section from "@/components/layout/Section";
import SineWaveTimeline from "@/components/SineWaveTimeline"; import SineWaveTimeline from "@/components/SineWaveTimeline";
import { useEffect, useRef, useState } from "react";
import Lenis from "lenis"
export default function AboutFounder() { export default function AboutFounder() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
@ -81,14 +83,74 @@ export default function AboutFounder() {
</div> </div>
</Section> </Section>
)} )}
<TimeLineComponent section4Data={section4Data} />
{section4Data && (
<section className={styles.section4Section} style={{ backgroundImage: section4Data?.backgroundImage ? `url(${section4Data?.backgroundImage})` : undefined }}>
<div className={styles.section4Title}>{section4Data?.title}</div>
<div className={styles.timelineWrapper}>
<SineWaveTimeline items={section4Data?.items ?? []} />
</div>
</section>)}
</div> </div>
); );
} }
function TimeLineComponent({ section4Data }: { section4Data: any }) {
const refElement = useRef<HTMLDivElement>(null);
const sectionRef = useRef<HTMLElement>(null);
const [isSectionInView, setIsSectionInView] = useState(false);
useEffect(() => {
if (!sectionRef.current) return;
const observer = new IntersectionObserver(([entry]) => {
setIsSectionInView(entry.isIntersecting);
}, {
threshold: 0.2,
});
observer.observe(sectionRef.current);
return () => {
observer.disconnect();
};
}, []);
useEffect(() => {
if (!isSectionInView || !refElement.current) return;
const wrapper = refElement.current;
const content = wrapper.firstElementChild as HTMLElement | null;
if (!content) return;
const lenis = new Lenis({
wrapper,
content,
orientation: "horizontal",
gestureOrientation: "both",
smoothWheel: true,
});
let rafId = 0;
const raf = (time: number) => {
lenis.raf(time);
rafId = requestAnimationFrame(raf);
};
rafId = requestAnimationFrame(raf);
return () => {
cancelAnimationFrame(rafId);
lenis.destroy();
};
}, [isSectionInView]);
return (
<>
{section4Data && (
<section
ref={sectionRef}
className={styles.section4Section}
style={{ backgroundImage: section4Data?.backgroundImage ? `url(${section4Data?.backgroundImage})` : undefined }}
>
<div className={styles.section4Title}>{section4Data?.title}</div>
<div className={styles.timelineWrapper}>
<SineWaveTimeline items={section4Data?.items ?? []} refElement={refElement} />
</div>
</section>
)}
</>
)
}

View File

@ -205,17 +205,20 @@
.featuresHeroTabRow { .featuresHeroTabRow {
display: flex; display: flex;
justify-content: space-evenly;
flex-direction: row; flex-direction: row;
justify-content: center; /* gap: 100px; */
gap: 200px;
} }
.featuresHeroTab { .featuresHeroTab {
font-weight: 500; font-weight: 500;
font-size: 20px; font-size: 20px;
color: #FFFFFF; color: #FFFFFF;
line-height: 60px;
height: 60px; height: 60px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer; cursor: pointer;
} }

View File

@ -16,6 +16,7 @@ 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 data = appConfig?.business?.commercialGroup; const data = appConfig?.business?.commercialGroup;
const section3Data = data?.section3Data; const section3Data = data?.section3Data;
@ -113,12 +114,12 @@ export default function BusinessCommercialGroup() {
)} )}
</div> </div>
<div className={styles.twoColText}> <div className={styles.twoColText}>
<p className={styles.twoColDesc}>{section2Data.tabItems[activeTabIndex]?.content}</p> <p className={styles.twoColDesc} dangerouslySetInnerHTML={{ __html: section2Data.tabItems[activeTabIndex]?.content ?? "" }}></p>
<Link <Link
to={section2Data.tabItems[activeTabIndex]?.path ?? "#"} to={section2Data.tabItems[activeTabIndex]?.path ?? "#"}
className={styles.btnPrimary} className={styles.btnPrimary}
> >
{viewDetail}
</Link> </Link>
</div> </div>
</div> </div>
@ -186,7 +187,7 @@ export default function BusinessCommercialGroup() {
<div className={styles.propertyServicesTitle}></div> <div className={styles.propertyServicesTitle}></div>
<p className={styles.propertyServicesSubtitle}></p> <p className={styles.propertyServicesSubtitle}></p>
<Link to="/property-service" className={styles.propertyServicesBtn}> <Link to="/property-service" className={styles.propertyServicesBtn}>
{viewDetail}
</Link> </Link>
</div> </div>
</section> </section>

View File

@ -22,6 +22,7 @@ type SelectOption = { label: string; value: string };
export default function JoinCampus() { export default function JoinCampus() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const { viewDetail="查看详情" } = appConfig?.__global__?.others
const supportLocales = useStore((s) => s.supportLocales); const supportLocales = useStore((s) => s.supportLocales);
const categoryList = useStore((s) => s.categoryList); const categoryList = useStore((s) => s.categoryList);
const locale = useStore((s) => s.locale); const locale = useStore((s) => s.locale);
@ -132,7 +133,7 @@ export default function JoinCampus() {
<div className={styles.jobItem}> <div className={styles.jobItem}>
<div className={styles.jobItemTitleRow}> <div className={styles.jobItemTitleRow}>
<div className={styles.jobItemTitle}>{item.title}</div> <div className={styles.jobItemTitle}>{item.title}</div>
<div className={styles.jobItemTitleRight}> <RightOutlined /></div> <div className={styles.jobItemTitleRight}>{viewDetail} <RightOutlined /></div>
</div> </div>
<div className={styles.jobItemLabels}> <div className={styles.jobItemLabels}>
{item.labels.map((label, index) => ( {item.labels.map((label, index) => (