This commit is contained in:
zhangjianjun 2026-03-20 18:32:51 +08:00
parent 4ca125387e
commit bc67d4a21d
14 changed files with 325 additions and 92 deletions

View File

@ -31,6 +31,28 @@ function App() {
}
}, [])
const getCategoryList = useCallback(async () => {
const results = await Promise.allSettled(
['news', 'job_type', 'job_area', 'job_unit', "file"]
.map(async (type) => {
const res = await appApi.getCategoryList(type);
return res.data.items.map((item:any) => {
return {
...item,
type
}
})
})
).then((results) => {
return results.map((result:any) => {
return result.value;
})
})
const categoryList = results.flat(Infinity)
useStore.getState().setCategoryList(categoryList);
}, [])
const getAppConfig = useCallback(async () => {
try {
const res = await appApi.getAppConfig();
@ -44,6 +66,8 @@ function App() {
];
useStore.getState().setSupportLocales(supportLocales);
initState(config);
await getCategoryList()
} catch (error) {
console.log(error);
}

View File

@ -24,14 +24,11 @@ const app = {
}
});
},
getDocList() {
getDocList(params: any) {
return requests({
url: "/yt/api/doc",
method: "get",
params: {
page: 1,
size: 1000,
}
params: params
});
},
// 历程列表

View File

@ -3,6 +3,17 @@
background-position: center;
background-repeat: no-repeat;
.cardImage {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
}
.cardVideo {
width: 100%;
height: 100%;
}
.cardMask {
position: absolute;
width: 100%;
@ -20,6 +31,7 @@
.cardTitle span {
left: 30px;
transform: translateX(0);
text-align: left;
transition-delay: 0s;
}
}
@ -45,8 +57,6 @@
transition-delay: 0.2s;
position: absolute;
top: calc(100% - 170px);
}
.cardTitle {
@ -61,11 +71,20 @@
padding: 0 30px;
span {
display: inline-block;
transition: all 0.3s ease;
transition-delay: 0.2s;
position: absolute;
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 100%;
box-sizing: border-box;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -1,23 +1,36 @@
import styles from './index.module.css';
import { Link } from 'react-router-dom';
import { useStore } from '@/store';
type Data = {
title: string;
content: string;
backgroundImage: string;
image: string;
video: string;
path: string;
moreText: string;
moreText: 'moreText';
}
export default function AnimateTopCard({ data }: { data: Data }) {
const appConfig = useStore((s) => s.appConfig);
const others = appConfig?.__global__?.others ?? {};
return (
<div className={styles.card} style={{ backgroundImage: `url(${data.backgroundImage})` }}>
<div className={styles.card}>
{
data.image ?
<img src={data.image} alt={data.title} className={styles.cardImage} /> :
<video src={data.video} autoPlay muted loop className={styles.cardVideo} />
}
<div className={styles.cardMask}></div>
<div className={styles.cardInner}>
<div className={styles.cardTitle}><span>{data.title}</span></div>
<div className={styles.cardTitleUnderline}></div>
<div className={styles.cardContent}>
<div>{data.content}</div>
<Link to={data.path} className={styles.cardMore}>{data.moreText}</Link>
<div dangerouslySetInnerHTML={{__html: data.content}}></div>
{
data.moreText &&
<Link to={data.path} className={styles.cardMore}>{others[data.moreText]}</Link>
}
</div>
</div>
</div>

View File

@ -23,16 +23,19 @@
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0%;
background: #fff;
transition: height 0.7s ease-in-out;
transition: height 0.5s ease-in-out;
}
.showDropPanel.header::before {
height: 350%;
height: 100%;
transition: height 0.2s ease-in-out;
}
.whiteMode.header::before {
height: 100%;
}
@ -44,11 +47,25 @@
transition-delay: none;
}
.whiteMode .navLink, .showDropPanel .navLink {
color: #222222;
.keepNavDark.header::before {
height: 120px;
}
.whiteMode, .showDropPanel {
.whiteMode .navLink,
.showDropPanel .navLink,
.keepNavDark .navLink,
.keepNavDark .langTrigger,
.keepNavDark .searchBtn,
.keepNavDark svg {
color: #222222;
/* transition: color 0.3s ease-in-out; */
}
.keepNavDark svg path {
fill: #222222;
}
.whiteMode,
.showDropPanel {
.searchBtn {
color: #222222;
}
@ -89,21 +106,27 @@
height: 1px;
background: #FFFFFF;
transform: scaleX(0);
transform-origin: 90% 0; /* 交点:左侧 85%,右侧 15%,从此点向左右展开 */
transform-origin: 90% 0;
/* 交点:左侧 85%,右侧 15%,从此点向左右展开 */
transition: transform 1.5s ease-in-out;
}
.animate {
&.headerInner::after {
transform: scaleX(1);
}
.crossYline {
height: 100%;
}
}
.whiteMode, .showDropPanel {
.whiteMode,
.showDropPanel {
.headerInner::after {
transform: scaleX(0);
}
.crossYline {
height: 0%;
}
@ -116,6 +139,12 @@
line-height: 1.3;
}
.commonMode {
.logo img {
transition: filter 0.5s steps(1);
}
}
.headerRight {
display: flex;
align-items: center;
@ -152,6 +181,7 @@
opacity: 0.9;
}
.actions {
display: flex;
height: 100%;
@ -164,8 +194,10 @@
width: 1px;
height: 0%;
background: #FFFFFF;
align-self: flex-end; /* 交点在最底部,从此点向上展开 */
transform-origin: center bottom; /* 缩放从底部中心点展开 */
align-self: flex-end;
/* 交点在最底部,从此点向上展开 */
transform-origin: center bottom;
/* 缩放从底部中心点展开 */
transition: height 1s ease-in-out;
}
@ -197,16 +229,18 @@
left: 0;
width: 100%;
height: 0;
/* background: rgba(255, 255, 255, 0.9); */
/* box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); */
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
padding: 0;
z-index: 1000;
overflow: hidden;
transition: height 0.5s ease-in-out, padding-top 0.5s ease-in-out;
/* transition-delay: 0.45s; */
&.visible {
height: 23.75rem;
padding-top: 1.25rem;
}
}

View File

@ -21,6 +21,8 @@ export default function Header() {
const [activeNav, setActiveNav] = useState("");
const [showDropPanel, setShowDropPanel] = useState(false);
const [keepNavDark, setKeepNavDark] = useState(false);
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [hoverElLeft, setHoverElLeft] = useState(0);
const handleNavEnter = (e: any, path: string) => {
const left = e.target.offsetLeft;
@ -29,6 +31,11 @@ export default function Header() {
setHoverElLeft(left + width / 2);
setActiveNav(path);
setShowDropPanel(true);
setKeepNavDark(true);
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
leaveTimerRef.current = null;
}
}
const activePanelItem = useMemo(() => {
@ -65,12 +72,33 @@ export default function Header() {
}, 500);
}, [])
const handleHeaderMouseLeave = () => {
setShowDropPanel(false);
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
}
leaveTimerRef.current = setTimeout(() => {
setKeepNavDark(false);
leaveTimerRef.current = null;
}, 500);
};
const handleHeaderMouseEnter = () => {
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
leaveTimerRef.current = null;
}
};
return (
<header className={`${styles.header}
${showWhiteMode && styles.whiteMode}
${showDropPanel && styles.showDropPanel}
${showWhiteMode ? styles.whiteMode : ''}
${showDropPanel ? styles.showDropPanel : ''}
${keepNavDark ? styles.keepNavDark : ''}
${!showWhiteMode && !showDropPanel ? styles.commonMode : ''}
`}
onMouseLeave={() => setShowDropPanel(false)}
onMouseLeave={handleHeaderMouseLeave}
onMouseEnter={handleHeaderMouseEnter}
>
<div className={`header-row ${styles.headerInner} ${animateHeader ? styles.animate : ""}`}>
<Link to="/" className={styles.logo}>
@ -117,7 +145,12 @@ export default function Header() {
</div>
<DropPanel items={activePanelItem} left={hoverElLeft}
onLinkClick={() => setShowDropPanel(false)}
onLinkClick={() => {
setShowDropPanel(false)
setTimeout(() => {
setKeepNavDark(false);
}, 500);
}}
show={showDropPanel && activePanelItem.length > 0}
/>
</header>

View File

@ -82,6 +82,7 @@ export default function Home() {
function News() {
const locale = useStore((s) => s.locale);
const categoryList = useStore((s) => s.categoryList);
const [newsData, setNewsData] = useState<any[]>([]);
const videoRef = useRef<HTMLVideoElement | null>(null);
const localNewsData = useMemo(() => {
@ -122,16 +123,10 @@ function News() {
setNewsData(data);
});
}, []);
const getCategoryList = useCallback(async () => {
const res = await appApi.getCategoryList('news');
const category_id = res.data.items.find((item: any) => item.name.includes('新闻资讯'))?.id;
return category_id;
}, []);
useEffect(() => {
getCategoryList().then((category_id) => {
const category_id = categoryList?.find((item: any) => item.name === '【首页】新闻资讯')?.id;
handleSearch(category_id)
});
}, [])
return (
<Section title="新闻资讯" maskBackground="#F0F2F4">

View File

@ -16,11 +16,14 @@ type JobItem = {
content: string;
labels: string[];
lang: string
}
};
type SelectOption = { label: string; value: string };
export default function JoinCampus() {
const appConfig = useStore((s) => s.appConfig);
const supportLocales = useStore((s) => s.supportLocales);
const categoryList = useStore((s) => s.categoryList);
const locale = useStore((s) => s.locale);
const data = appConfig?.join?.campus;
const banner = data?.banner;
@ -29,11 +32,11 @@ export default function JoinCampus() {
// 职业类别 业务领域 所属板块
const [jobType, setJobType] = useState('');
const [jobTypeOptions, setJobTypeOptions] = useState([]);
const [jobTypeOptions, setJobTypeOptions] = useState<SelectOption[]>([]);
const [businessArea, setBusinessArea] = useState('');
const [businessAreaOptions, setBusinessAreaOptions] = useState([]);
const [businessAreaOptions, setBusinessAreaOptions] = useState<SelectOption[]>([]);
const [businessPlate, setBusinessPlate] = useState('');
const [businessPlateOptions, setBusinessPlateOptions] = useState([]);
const [businessPlateOptions, setBusinessPlateOptions] = useState<SelectOption[]>([]);
const [page, setPage] = useState(1);
const [size] = useState(2 * supportLocales.length);
@ -69,20 +72,16 @@ export default function JoinCampus() {
}, 500), []);
const getTypes = useCallback(() => {
['job_type', 'job_area', 'job_unit'].forEach(type => {
appApi.getCategoryList(type).then((res) => {
const items = res.data.items.map((item:any) => ({ label: item.name, value: item.id }));
items.unshift({ label: "全部", value: "" });
if (type === 'job_type') {
setJobTypeOptions(items);
} else if (type === 'job_area') {
setBusinessAreaOptions(items);
} else if (type === 'job_unit') {
setBusinessPlateOptions(items);
}
})
})
}, []);
const jobTypeOptions: SelectOption[] =
categoryList?.filter((item: any) => item.type === 'job_type').map((item: any) => ({ label: item.name, value: String(item.id) })) ?? [];
const businessAreaOptions: SelectOption[] =
categoryList?.filter((item: any) => item.type === 'job_area').map((item: any) => ({ label: item.name, value: String(item.id) })) ?? [];
const businessPlateOptions: SelectOption[] =
categoryList?.filter((item: any) => item.type === 'job_unit').map((item: any) => ({ label: item.name, value: String(item.id) })) ?? [];
setJobTypeOptions(jobTypeOptions);
setBusinessAreaOptions(businessAreaOptions);
setBusinessPlateOptions(businessPlateOptions);
}, [categoryList]);
useEffect(() => {
refreshData();

View File

@ -21,19 +21,20 @@ export default function NewsPublic() {
const appConfig = useStore((s) => s.appConfig);
const locale = useStore((s) => s.locale);
const supportLocales = useStore((s) => s.supportLocales);
const categoryList = useStore((s) => s.categoryList);
const data = appConfig?.news?.public;
const [page, setPage] = useState(1);
const [size] = useState(9 * supportLocales.length);
const [total, setTotal] = useState(0);
const categoryIdRef = useRef<string | null>('');
const categoryId = String(categoryList?.find((item: any) => item.name.includes('集团发布'))?.id ?? '');
const [newList, setNewList] = useState<NewsItem[]>([]);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
const [searchValue, setSearchValue] = useState("");
const handleSearch = useCallback(() => {
appApi.getNewsList({ page, size, sort: "create_time DESC", title: searchValue,
category_id: categoryIdRef.current ?? '',
category_id: categoryId,
}).then((res) => {
const data = res.data.items.map((item:any) => {
return {
@ -54,19 +55,10 @@ export default function NewsPublic() {
return newList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
}, [newList, locale]);
const getCategoryList = useCallback(async () => {
const res = await appApi.getCategoryList('news');
const category_id = res.data.items.find((item: any) => item.name.includes('集团发布'))?.id;
categoryIdRef.current = category_id;
return category_id;
}, []);
const banner = data?.banner;
useEffect(() => {
getCategoryList().then(() => {
handleSearch();
})
}, [page, size]);
if (!data) return null;

View File

@ -1,5 +1,5 @@
import Banner, { type BannerConfig } from "@/components/Banner";
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import ParagraphSection from "@/components/layout/ParagraphSection";
import Section from "@/components/layout/Section";
import AnimateTopCard from "@/components/layout/AnimateTopCard";
@ -7,20 +7,44 @@ import BottomTabs from "@/components/layout/BottomTabsSection/BottomTabs";
import { useStore } from "@/store";
import styles from "./Foundation.module.css";
import TopTabsSection from "@/components/layout/TopTabsSection";
import appApi from "@/api/app";
export default function Foundation() {
const appConfig = useStore((s) => s.appConfig);
const data = appConfig?.social?.foundation;
const locale = useStore((s) => s.locale)
const categoryList = useStore((s) => s.categoryList)
const [activeIndex, setActiveIndex] = useState(0);
if (!data) return null;
const banner = data.banner;
const section1Data = data.section1Data;
const section2Data = data.section2Data;
const section3Data = data.section3Data;
const section4Data = data.section4Data;
const [newsList, setNewsList] = useState<any[]>([]);
const localNewsList = useMemo(() => {
return newsList.filter((item: any) => item.lang.toLowerCase() === locale.split('-')[0]);
}, [newsList, locale])
useEffect(() => {
appApi.getNewsList({ page: 1, size: 1000, sort: "create_time DESC",
category_id: String(categoryList?.find((item: any) => item.name.includes('【可持续发展】社会责任案例集'))?.id ?? ''),
}).then((res:any) => {
setNewsList(res.data.items.map((item:any) => {
return {
id: item.id,
title: item.title,
content: item.content,
path: item.path,
image: item.covers_show === 'image' ? item.covers.image[0] : '',
video: item.covers_show === 'video' ? item.covers.video[0] : '',
lang: item.lang,
moreText: 'moreText',
}
}));
});
}, [])
return (
<div>
<Banner
@ -43,13 +67,12 @@ export default function Foundation() {
maskBackground="rgba(255,255,255,0.3)"
>
<div className={styles.publicWelfareDataItems}>
{section2Data.items?.map((item: { title: string; content?: string; backgroundImage?: string; path?: string; moreText?: string }, index: number) => (
{localNewsList?.map((item: any, index: number) => (
<div
key={index}
className={styles.publicWelfareDataItem}
style={{ backgroundImage: `url(${item.backgroundImage})` }}
>
<AnimateTopCard data={{ ...item, content: item.content ?? "", path: item.path ?? "", moreText: item.moreText ?? "", backgroundImage: item.backgroundImage ?? "" }} />
<AnimateTopCard data={item} />
</div>
))}
</div>

View File

@ -140,6 +140,7 @@
background-size: cover;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
.socialResponsibilityReportItemTitle {

View File

@ -1,18 +1,30 @@
import Banner, { type BannerConfig } from "@/components/Banner";
import { useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import ParagraphSection from "@/components/layout/ParagraphSection";
import ColumnXGrids from "@/components/layout/ColumnXGrids";
import Section from "@/components/layout/Section";
import AnimateTopCard from "@/components/layout/AnimateTopCard";
import { useStore } from "@/store";
import styles from "./Sustainability.module.css";
import appApi from "@/api/app";
type NewsItem = {
id: number;
title: string;
createTime: string;
image: string;
video: string;
lang: "ZH" | "EN"
}
const LANG_LEN = 2
export default function Sustainability() {
const appConfig = useStore((s) => s.appConfig);
const categoryList = useStore((s) => s.categoryList);
const locale = useStore((s) => s.locale);
const data = appConfig?.social?.sustainability;
const [sliceIndex, setSliceIndex] = useState(4);
if (!data) return null;
const others = appConfig?.__global__?.others ?? {};
const banner = data.banner;
const section1Data = data.section1Data;
@ -22,6 +34,81 @@ export default function Sustainability() {
const columnXGridsData = section2Data;
// 社会责任案例新闻数据
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
const localNewsItems = useMemo(() => {
return newsItems.filter((item: any) => item.lang.toLowerCase() === locale.split('-')[0]);
}, [newsItems, locale])
const getNewsList = useCallback(() => {
appApi.getNewsList({
page: 1,
size: 8,
sort: "create_time DESC",
category_id: String(categoryList?.find((item: any) => item.name.includes('【可持续发展】社会责任案例集'))?.id ?? ''),
}).then((res) => {
const items = res.data.items.map((item: any) => {
return {
id: item.id,
title: item.title,
content: item.content,
createTime: item.create_time,
image: item.covers_show === 'image' ? item.covers.image[0] : '',
video: item.covers_show === 'video' ? item.covers.video[0] : '',
path: `/news/detail/${item.id}`,
lang: item.lang,
moreText: 'moreText',
}
})
setNewsItems(items as NewsItem[])
});
}, [])
// 社会责任报告
type ReportItem = {
id: number;
name: string;
path: string;
cover: string;
}
const [reportItems, setReportItems] = useState<ReportItem[]>([]);
const [page, setPage] = useState(1);
const [size, setSize] = useState(8);
const [total, setTotal] = useState(0);
const localReportItems = useMemo(() => {
return reportItems.filter((item: any) => item.lang.toLowerCase() === locale.split('-')[0]);
}, [reportItems, locale])
const getReportList = useCallback(async () => {
const res = await appApi.getDocList({
page,
size,
category_id: String(categoryList?.find((item: any) => item.name.includes('社会责任报告'))?.id ?? '')
})
setTotal(res.data.total / LANG_LEN)
setReportItems((prev) => {
let items = [...prev, ...res.data.items]
const resItems: any[] = []
// 去重 id lang 相同
items.forEach((item: any) => {
if(!resItems.some((i: any) => i.id === item.id && i.lang === item.lang)) {
resItems.push(item)
}
})
console.log('---resItems', resItems)
return resItems
})
}, [page, size])
useEffect(() => {
getNewsList()
}, []);
useEffect(() => {
getReportList()
}, [page])
return (
<div>
<Banner
@ -47,11 +134,10 @@ export default function Sustainability() {
{section3Data.content}
</p>
<div className={styles.socialResponsibilityCaseDataItems}>
{section3Data.items?.map((item: any, index: number) => (
{localNewsItems?.map((item: any, index: number) => (
<div
key={index}
className={styles.socialResponsibilityCaseDataItem}
style={{ backgroundImage: `url(${item.backgroundImage})` }}
>
<AnimateTopCard data={item} />
</div>
@ -67,32 +153,36 @@ export default function Sustainability() {
maskBackground="#F7FBFF"
>
<div className={styles.socialResponsibilityReportData}>
{section4Data.items?.slice(0, sliceIndex).map((item: any, index: number) => (
{localReportItems?.map((item: any, index: number) => (
<div
key={index}
className={styles.socialResponsibilityReportItem}
>
<div
className={styles.socialResponsibilityReportItemCover}
style={{ backgroundImage: `url(${item.coverImage})` }}
style={{ backgroundImage: `url(${item.cover})` }}
onClick={() => {
window.open(item.path, '_blank')
}}
/>
<div className={styles.socialResponsibilityReportItemTitle}>
{item.title}
{item.name}
</div>
</div>
))}
</div>
<div
className={styles.socialResponsibilityReportItemMore}
onClick={() =>
setSliceIndex(
sliceIndex < (section4Data.items?.length ?? 0)
? sliceIndex + 4
: 4
)
onClick={() => {
if(localReportItems.length < total) {
setPage(page + 1)
} else {
setReportItems(prev => [...prev.slice(0, 8)])
setPage(1)
}
}}
>
{sliceIndex < (section4Data.items?.length ?? 0) ? "了解更多" : "收起"}
{localReportItems.length < total ? "了解更多" : "收起"}
</div>
</Section>
)}

View File

@ -15,10 +15,16 @@ interface StoreState {
locale: LocaleKey;
i18nData: I18nData | null;
appConfig: AppConfig | null;
categoryList: {
id: number;
name: string;
type: string;
}[] | null;
supportLocales: SupportLocale[];
token: string | null;
setLocale: (locale: LocaleKey) => void;
setAppConfig: (data: I18nData) => void;
setCategoryList: (list: any[]) => void;
setToken: (token: string | null) => void;
setSupportLocales: (locales: SupportLocale[]) => void;
}
@ -29,6 +35,7 @@ export const useStore = create<StoreState>()(
locale: 'zh-CN',
i18nData: null,
appConfig: null,
categoryList: null,
supportLocales: [],
token: null,
setLocale: (locale) =>
@ -41,6 +48,7 @@ export const useStore = create<StoreState>()(
i18nData: data,
appConfig: data[state.locale] ?? data['en-US'] ?? data['zh-CN'] ?? null,
})),
setCategoryList: (list) => set({ categoryList: list }),
setToken: (token) => set({ token }),
setSupportLocales: (locales: SupportLocale[]) => set({ supportLocales: locales }),
}),
@ -62,6 +70,7 @@ export const useStore = create<StoreState>()(
locale,
i18nData: { "zh-CN": legacy, "en-US": legacy },
appConfig: legacy,
categoryList: p.categoryList,
};
},
partialize: (s) => ({
@ -70,6 +79,7 @@ export const useStore = create<StoreState>()(
token: s.token,
appConfig: s.i18nData?.[s.locale] ?? s.appConfig,
supportLocales: s.supportLocales,
categoryList: s.categoryList,
}),
}
)

View File

@ -1,3 +1,6 @@
import { useStore } from "zustand";
import { LocaleKey } from "@/type";
// debounce
export const debounce = (func: (...args: any[]) => void, delay: number) => {
let timeout: NodeJS.Timeout;