save
This commit is contained in:
parent
3f5380f9c4
commit
fabe5c893f
|
|
@ -45,6 +45,7 @@
|
|||
"jest": "^27.4.3",
|
||||
"jest-resolve": "^27.4.2",
|
||||
"jest-watch-typeahead": "^1.0.0",
|
||||
"lenis": "^1.3.19",
|
||||
"mime": "^4.0.7",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"motion": "^12.23.25",
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
"postcss-preset-env": "^7.0.1",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^19.1.0",
|
||||
"react-activation": "^0.13.4",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^19.1.0",
|
||||
|
|
@ -79,8 +81,7 @@
|
|||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"winston": "^3.17.0",
|
||||
"workbox-webpack-plugin": "^6.4.1",
|
||||
"zustand": "^5.0.11",
|
||||
"react-activation": "^0.13.4"
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node --stack-size=12800 --stack-trace-limit=20 scripts/start.js",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
cursor: all-scroll;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type TimelineItem = {
|
|||
type Props = {
|
||||
items: TimelineItem[];
|
||||
height?: number;
|
||||
refElement?: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
function generateSinePath(
|
||||
|
|
@ -101,7 +102,7 @@ function computeContentWidth(items: TimelineItem[]): number {
|
|||
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;
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className={styles.scrollContainer}>
|
||||
<div className={styles.scrollContainer} ref={refElement}>
|
||||
<div
|
||||
className={styles.content}
|
||||
style={{ width: contentWidth, height: `${expandedHeight}px` }}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,22 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,15 +6,25 @@ type Data = {
|
|||
content: string;
|
||||
statsData?: { num: string; label: 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 (
|
||||
<section className={styles.paragraphSection} style={{ backgroundImage: `url(${data.backgroundImage})` }}>
|
||||
<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>
|
||||
{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}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@
|
|||
.statLabel {
|
||||
font-size: 1rem;
|
||||
color: var(--stats-label-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import styles from './index.module.css';
|
||||
import { useStore } from '@/store';
|
||||
|
||||
type Data = {
|
||||
tabItems: {
|
||||
|
|
@ -19,6 +20,8 @@ type Data = {
|
|||
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 scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -99,9 +102,17 @@ export default function TopTabs({ data, activeIndex, setActiveIndex, className }
|
|||
};
|
||||
}, [activeIndex, data.tabItems.length, updateIndicatorPosition, updateScrollState]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log('locale', locale);
|
||||
updateIndicatorPosition();
|
||||
}, [locale]);
|
||||
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div ref={containerRef} className={styles.topTabsTabs}>
|
||||
<div ref={containerRef} className={styles.topTabsTabs}
|
||||
>
|
||||
{canScrollLeft && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
/* 隐藏滚动条 */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -159,3 +160,23 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import styles from './index.module.css';
|
|||
import TopTabs from './TopTabs';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
|
||||
type Data = {
|
||||
tabItems: {
|
||||
icon?: string;
|
||||
|
|
@ -14,6 +15,7 @@ type Data = {
|
|||
/** 以 mockData 为准 */
|
||||
sideImage?: string;
|
||||
path?: string;
|
||||
items?: {label: string}[];
|
||||
}[]
|
||||
backgroundImage?: string;
|
||||
titleDirection?: 'row' | 'column';
|
||||
|
|
@ -29,7 +31,6 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
|
|||
if (id && data.tabItems) {
|
||||
setTimeout(() => {
|
||||
const index = data.tabItems.findIndex((item) => item.tabName === id);
|
||||
console.log('index', index, id, data.tabItems)
|
||||
if (index !== -1) {
|
||||
setActiveIndex(index);
|
||||
}
|
||||
|
|
@ -40,25 +41,41 @@ export default function TopTabsSection({ data, className }: { data: Data, classN
|
|||
<section id={id} className={`${styles.topTabsSection} ${className}`} style={{ backgroundImage: `url(${data.backgroundImage})` }}>
|
||||
<TopTabs data={data} activeIndex={activeIndex} setActiveIndex={setActiveIndex} />
|
||||
<div className={styles.topTabsContent}>
|
||||
<div className={styles.topTabsContentLeft}>
|
||||
<div className={styles.topTabsContentLeftHead}>
|
||||
{
|
||||
data.tabItems[activeIndex].icon && (
|
||||
<img src={data.tabItems[activeIndex].icon} alt="" style={{ width: '100px', height: '100px' }} />
|
||||
)
|
||||
}
|
||||
<div className={`${styles.topTabsContentLeftTitle} ${data.titleDirection === 'column' ? styles.columnCenter : ''}`}>
|
||||
<div className={styles.topTabsContentLeftTitleMain}>{data.tabItems[activeIndex].contentTitle}</div>
|
||||
<div className={styles.topTabsContentLeftTitleSub}>{data.tabItems[activeIndex].contentSubtitle}</div>
|
||||
{
|
||||
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.topTabsContentLeftHead}>
|
||||
{
|
||||
data.tabItems[activeIndex].icon && (
|
||||
<img src={data.tabItems[activeIndex].icon} alt="" style={{ width: '100px', height: '100px' }} />
|
||||
)
|
||||
}
|
||||
<div className={`${styles.topTabsContentLeftTitle} ${data.titleDirection === 'column' ? styles.columnCenter : ''}`}>
|
||||
<div className={styles.topTabsContentLeftTitleMain}>{data.tabItems[activeIndex].contentTitle}</div>
|
||||
<div className={styles.topTabsContentLeftTitleSub}>{data.tabItems[activeIndex].contentSubtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.topTabsContentLeftDesc}>
|
||||
{data.tabItems[activeIndex].contentText ?? data.tabItems[activeIndex].content}
|
||||
</p>
|
||||
</div>
|
||||
<p className={styles.topTabsContentLeftDesc}>
|
||||
{data.tabItems[activeIndex].contentText ?? data.tabItems[activeIndex].content}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.topTabsContentRight}>
|
||||
<img src={data.tabItems[activeIndex].sideImage} alt="side-image" />
|
||||
</div>
|
||||
<div className={styles.topTabsContentRight}>
|
||||
<img src={data.tabItems[activeIndex].sideImage} alt="side-image" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import ParagraphSection from "@/components/layout/ParagraphSection";
|
|||
import { useStore } from "@/store";
|
||||
import Section from "@/components/layout/Section";
|
||||
import SineWaveTimeline from "@/components/SineWaveTimeline";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Lenis from "lenis"
|
||||
|
||||
export default function AboutFounder() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
|
|
@ -71,24 +73,84 @@ export default function AboutFounder() {
|
|||
{section3Data && (
|
||||
<Section title={section3Data?.title} titleColor="#fff" background={section3Data?.backgroundImage}>
|
||||
<div className={styles.section3Content}>
|
||||
{
|
||||
section3Data?.items.map((item: { title?: string }, index: number) => (
|
||||
<div key={index} className={styles.section3Item}>
|
||||
{item.title ? <li>{item.title}</li> : null}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
section3Data?.items.map((item: { title?: string }, index: number) => (
|
||||
<div key={index} className={styles.section3Item}>
|
||||
{item.title ? <li>{item.title}</li> : null}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{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>)}
|
||||
<TimeLineComponent section4Data={section4Data} />
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -205,17 +205,20 @@
|
|||
|
||||
.featuresHeroTabRow {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 200px;
|
||||
/* gap: 100px; */
|
||||
}
|
||||
|
||||
.featuresHeroTab {
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
line-height: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function PlaceholderImage() {
|
|||
|
||||
export default function BusinessCommercialGroup() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const { viewDetail="查看详情" } = appConfig?.__global__?.others
|
||||
const data = appConfig?.business?.commercialGroup;
|
||||
const section3Data = data?.section3Data;
|
||||
|
||||
|
|
@ -113,12 +114,12 @@ export default function BusinessCommercialGroup() {
|
|||
)}
|
||||
</div>
|
||||
<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
|
||||
to={section2Data.tabItems[activeTabIndex]?.path ?? "#"}
|
||||
className={styles.btnPrimary}
|
||||
>
|
||||
查看详情
|
||||
{viewDetail}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,7 +187,7 @@ export default function BusinessCommercialGroup() {
|
|||
<div className={styles.propertyServicesTitle}>物业服务</div>
|
||||
<p className={styles.propertyServicesSubtitle}>国内一流物业服务品牌</p>
|
||||
<Link to="/property-service" className={styles.propertyServicesBtn}>
|
||||
查看详情
|
||||
{viewDetail}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type SelectOption = { label: string; value: string };
|
|||
|
||||
export default function JoinCampus() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const { viewDetail="查看详情" } = appConfig?.__global__?.others
|
||||
const supportLocales = useStore((s) => s.supportLocales);
|
||||
const categoryList = useStore((s) => s.categoryList);
|
||||
const locale = useStore((s) => s.locale);
|
||||
|
|
@ -132,7 +133,7 @@ export default function JoinCampus() {
|
|||
<div className={styles.jobItem}>
|
||||
<div className={styles.jobItemTitleRow}>
|
||||
<div className={styles.jobItemTitle}>{item.title}</div>
|
||||
<div className={styles.jobItemTitleRight}>查看详情 <RightOutlined /></div>
|
||||
<div className={styles.jobItemTitleRight}>{viewDetail} <RightOutlined /></div>
|
||||
</div>
|
||||
<div className={styles.jobItemLabels}>
|
||||
{item.labels.map((label, index) => (
|
||||
|
|
|
|||
Loading…
Reference in New Issue