This commit is contained in:
zhangjianjun 2026-03-17 18:57:02 +08:00
parent 1c1c68d597
commit 5cab4c00eb
88 changed files with 1214 additions and 1324 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="92" height="55" viewBox="0 0 92 55"><defs><pattern x="0" y="0" width="92" height="55" patternUnits="userSpaceOnUse" id="master_svg0_94_11479"><image x="0" y="0" width="92" height="55" xlink:href="data:image/png;base64,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"/></pattern></defs><rect x="0" y="0" width="92" height="55" rx="0" fill="url(#master_svg0_94_11479)" fill-opacity="1"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26"><defs><clipPath id="master_svg0_199_42651"><rect x="0" y="0" width="26" height="26" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_199_42651)"><path d="M17.598751,8.817256891796875C17.897751,8.817256891796875,18.191875,8.840007291796875,18.487625,8.872507091796875C17.691374,5.164257091796875,13.728,2.409881591796875,9.2023745,2.409881591796875C4.1437502,2.409881839752215,0,5.856507091796875,0,10.235882791796875C0,12.762757591796875,1.3780001,14.837882591796875,3.6822503,16.449883591796876L2.7625,19.217260591796876L5.9799995,17.605257591796875C7.1304998,17.831133591796878,8.0535002,18.065133591796876,9.2023754,18.065133591796876C9.4916248,18.065133591796876,9.777626,18.052132591796877,10.060375,18.031010591796875C9.8816242,17.415133591796874,9.776,16.771634591796875,9.776,16.100510591796876C9.777626,12.080258391796875,13.229124,8.817256891796875,17.598751,8.817256891796875ZM12.65225,6.322882191796875C13.34775,6.322882191796875,13.804376,6.779507591796875,13.804376,7.471756891796875C13.804376,8.160757091796874,13.347751,8.622257191796875,12.65225,8.622257191796875C11.964874,8.622257191796875,11.272624,8.160757091796874,11.272624,7.471756891796875C11.272626,6.777882091796875,11.963251,6.322882191796875,12.65225,6.322882191796875ZM6.2123752,8.622257191796875C5.52175,8.622257191796875,4.8262506,8.160757091796874,4.8262506,7.471756891796875C4.8262506,6.779506691796875,5.52175,6.322881691796875,6.2123752,6.322881691796875C6.9030004,6.322881691796875,7.3612499,6.777882091796875,7.3612499,7.471756891796875C7.3612499,8.160757491796875,6.9030004,8.622257191796875,6.2123752,8.622257191796875ZM26,15.990007591796875C26,12.311007491796875,22.317751,9.312882391796876,18.182125,9.312882391796876C13.802751,9.312882391796876,10.356126,12.312631591796874,10.356126,15.990007591796875C10.356126,19.678758591796875,13.804376,22.667135591796875,18.182125,22.667135591796875C19.098627,22.667135591796875,20.023252,22.438007591796875,20.943001,22.207260591796874L23.466627,23.591758591796875L22.774376,21.290756591796875C24.622,19.903009591796874,26,18.065132591796875,26,15.990007591796875ZM15.643875,14.837882591796875C15.18725,14.837882591796875,14.724126,14.382882591796875,14.724126,13.918131591796875C14.724126,13.459882591796875,15.18725,12.998382591796876,15.643875,12.998382591796876C16.342625,12.998382591796876,16.796001,13.459883591796874,16.796001,13.918131591796875C16.796001,14.382882591796875,16.342625,14.837882591796875,15.643875,14.837882591796875ZM20.705751,14.837882591796875C20.252375,14.837882591796875,19.790874,14.382882591796875,19.790874,13.918131591796875C19.790874,13.459882591796875,20.25075,12.998382591796876,20.705751,12.998382591796876C21.397999,12.998382591796876,21.857874,13.459883591796874,21.857874,13.918131591796875C21.857874,14.382882591796875,21.398003,14.837882591796875,20.705751,14.837882591796875Z" fill="#212529" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="92" height="55" viewBox="0 0 92 55"><defs><pattern x="0" y="0" width="92" height="55" patternUnits="userSpaceOnUse" id="master_svg0_94_11479"><image x="0" y="0" width="92" height="55" xlink:href="data:image/png;base64,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"/></pattern></defs><rect x="0" y="0" width="92" height="55" rx="0" fill="url(#master_svg0_94_11479)" fill-opacity="1"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -124,4 +124,17 @@ body {
color: #222222; color: #222222;
line-height: 34px; line-height: 34px;
margin-bottom: 20px; margin-bottom: 20px;
}
/* hover缩放 */
.hover-scale {
.hover-scale-bg {
transition: transform 0.8s ease;
}
&:hover {
.hover-scale-bg {
transform: scale(1.05);
}
}
} }

View File

@ -5,6 +5,7 @@ import routes from "@/Routes";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import appApi from "@/api/app"; import appApi from "@/api/app";
import { useStore } from "@/store"; import { useStore } from "@/store";
import { parsePageConfig } from "@/utils/parsePageConfig";
import type { I18nData, SupportLocale } from "@/type"; import type { I18nData, SupportLocale } from "@/type";
function AppRoutes() { function AppRoutes() {
@ -20,7 +21,6 @@ function App() {
const company = data?.company as { config?: { favicon?: string; shortName?: string } } | undefined; const company = data?.company as { config?: { favicon?: string; shortName?: string } } | undefined;
const { favicon, shortName } = company?.config || {}; const { favicon, shortName } = company?.config || {};
if (favicon) { if (favicon) {
// 替换网页图标
const favor: any = document.querySelector("link[rel*='icon']") || document.createElement('link'); const favor: any = document.querySelector("link[rel*='icon']") || document.createElement('link');
favor.type = 'image/x-icon'; favor.type = 'image/x-icon';
favor.rel = 'shortcut icon'; favor.rel = 'shortcut icon';
@ -34,10 +34,10 @@ function App() {
const getAppConfig = useCallback(async () => { const getAppConfig = useCallback(async () => {
try { try {
const res = await appApi.getAppConfig(); const res = await appApi.getAppConfig();
const data = res.data as I18nData; const data = parsePageConfig(res.data.items) as I18nData;
useStore.getState().setAppConfig(data); useStore.getState().setAppConfig(data);
const locale = useStore.getState().locale; const currentLocale = useStore.getState().locale;
const config = data[locale] ?? data["zh-CN"]; const config = data[currentLocale] ?? data["zh-CN"];
const supportLocales: SupportLocale[] = [ const supportLocales: SupportLocale[] = [
{ key: "zh-CN", label: "中文" }, { key: "zh-CN", label: "中文" },
{ key: "en-US", label: "English" }, { key: "en-US", label: "English" },

View File

@ -54,11 +54,11 @@ const routes: RouteObject[] = [
{ path: "join/culture", element: <JoinCulture /> }, { path: "join/culture", element: <JoinCulture /> },
{ path: "join/campus", element: <JoinCampus /> }, { path: "join/campus", element: <JoinCampus /> },
{ path: "join/campus/detail/:id", element: <JoinCampusDetail /> }, { path: "join/campus/detail/:id", element: <JoinCampusDetail /> },
// 其它
// 物业服务 // 物业服务
{ path: "/property-service", element: <PropertyService /> }, { path: "/property-service", element: <PropertyService /> },
// 搜索 // 搜索
{ path: "/search", element: <Search /> }, { path: "/search", element: <Search /> },
// 其它
// 使用条款 隐私保护 审计举报 网站地图 // 使用条款 隐私保护 审计举报 网站地图
{ path: "/terms-of-use", element: <TermsOfUse /> }, { path: "/terms-of-use", element: <TermsOfUse /> },
{ path: "/privacy-policy", element: <PrivacyPolicy /> }, { path: "/privacy-policy", element: <PrivacyPolicy /> },

View File

@ -1,23 +1,74 @@
import requests from "@/utils/request"; import requests from "@/utils/request";
import mockData from "./mockData"
type Params = {page:number, size:number, sort?:string}
const app = { const app = {
getAppConfig() { getAppConfig() {
// return requests({
// url: "/api/config",
// method: "get",
// });
return Promise.resolve({data: mockData});
},
getJobList(params: any) {
return requests({ return requests({
url: "/api/job", url: "/yt/api/page",
method: "get",
params: {
page: 1,
size: 1000,
}
});
},
// 分类
getCategoryList(type:string) {
return requests({
url: "/yt/api/category",
method: "get",
params: {
page: 1,
size: 1000,
type
}
});
},
getDocList() {
return requests({
url: "/yt/api/doc",
method: "get",
params: {
page: 1,
size: 1000,
}
});
},
// 历程列表
getProcessList() {
return requests({
url: "/yt/api/process",
method: "get",
params: {
page: 1,
size: 1000,
}
});
},
// 新闻列表
getNewsList(params: Params & { title?: string, category_id?: string }) {
return requests({
url: "/yt/api/news",
method: "get",
params
});
},
getNewsDetail(id: number | string) {
return requests({
url: `/yt/api/news/${id}`,
method: "get",
});
},
getJobList(params: Params & { title?: string, job_type?: string, business_area?: string, business_plate?: string }) {
return requests({
url: "/yt/api/job",
method: "get", method: "get",
params params
}); });
}, },
getJobDetail(id: number | string) { getJobDetail(id: number | string) {
return requests({ return requests({
url: `/api/job/${id}`, url: `/yt/api/job/${id}`,
method: "get", method: "get",
}); });
}, },

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
.screenOpen {
width: 100%;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
.screenOpenContent {
display: flex;
flex-direction: row;
overflow: hidden;
img {
width: 100px;
}
}
.screenOpenBackground {
width: 100%;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: -1;
.screenOpenBackgroundItem {
width: 100%;
height: 33.333%;
background-color: #fff;
}
}
}

View File

@ -0,0 +1,57 @@
import styles from "./index.module.css";
import { motion } from "framer-motion";
import { useState } from "react";
import { useAppStore } from '@/store/app'
export default function ScreenOpen() {
const screenOpened = useAppStore((s) => s.screenOpened);
const setScreenOpened = useAppStore((s) => s.setScreenOpened);
const [data] = useState({
logoParts: ["/images/open-screen/logo-left.png", "/images/open-screen/logo-right.png"],
})
if(screenOpened) return null;
return (
<div className={styles.screenOpen}>
<div className={styles.screenOpenContent}>
{
data.logoParts.map((part, index) => (
<motion.div
key={index}
className={styles.screenOpenBackgroundItem}
initial={{ y: 0 }}
animate={{ y: index === 0 ? "-100%" : "100%" }}
transition={{
delay: 1.4,
duration: 0.6,
ease: "easeInOut",
}}
>
<img src={part} alt="logo-part" />
</motion.div>
))
}
</div>
{/* 3条白色背景 */}
<div className={styles.screenOpenBackground}>
{[0, 1, 2].map((index) => (
<motion.div
key={index}
className={styles.screenOpenBackgroundItem}
initial={{ x: 0 }}
animate={{ x: "100%" }}
transition={{
delay: 2 + index * 0.15,
duration: 0.6,
ease: "easeInOut",
}}
// 动画完成时移除组件
onAnimationComplete={() => {
setScreenOpened(true);
}}
/>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,38 @@
/**
*
* ScrollReveal
*/
export type AnimationPreset =
| "fadeIn"
| "slideUp"
| "slideDown"
| "slideLeft"
| "slideRight";
export type VariantState = {
hidden: Record<string, unknown>;
visible: Record<string, unknown>;
};
export const ANIMATION_VARIANTS: Record<AnimationPreset, VariantState> = {
fadeIn: {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
slideUp: {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0 },
},
slideDown: {
hidden: { opacity: 0, y: -40 },
visible: { opacity: 1, y: 0 },
},
slideLeft: {
hidden: { opacity: 0, x: 60 },
visible: { opacity: 1, x: 0 },
},
slideRight: {
hidden: { opacity: 0, x: -60 },
visible: { opacity: 1, x: 0 },
},
};

View File

@ -0,0 +1,56 @@
import { motion, Variants } from "motion/react";
import {
ANIMATION_VARIANTS,
type AnimationPreset,
} from "./animationPresets";
export type ScrollRevealProps = {
/** 动画预设:淡入、上/下/左/右滑入 */
preset?: AnimationPreset;
children: React.ReactNode;
className?: string;
/** 是否只触发一次(进入视口后不再反向动画),默认 true */
once?: boolean;
/** 元素进入视口的比例阈值 0~1默认 0.2 表示露出 20% 即触发 */
amount?: number;
/** 动画延迟(秒) */
delay?: number;
/** 动画时长(秒),默认 0.6 */
duration?: number;
/** 作为何种标签渲染,默认 div */
as?: keyof typeof motion;
};
export default function ScrollReveal({
preset = "fadeIn",
children,
className,
once = true,
amount = 0.5,
delay = 0,
duration = 0.7,
as: Component = "div",
}: ScrollRevealProps) {
const variant = ANIMATION_VARIANTS[preset];
const MotionEl = motion[Component] as typeof motion.div;
return (
<MotionEl
initial="hidden"
whileInView="visible"
viewport={{ once, amount }}
variants={variant as Variants}
transition={{
duration,
delay,
ease: [0.25, 0.46, 0.45, 0.94],
}}
className={className}
>
{children}
</MotionEl>
);
}
export { ANIMATION_VARIANTS };
export type { AnimationPreset } from "./animationPresets";

View File

@ -6,7 +6,8 @@ import "swiper/css/effect-fade";
import styles from "./Banner.module.css"; import styles from "./Banner.module.css";
import { useMemo } from "react"; import { useMemo } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import type { BannerConfig } from "@/type"; import type { BannerConfig, NavItem, NavChild } from "@/type";
import ScrollReveal from "@/components/ScrollReveal";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)"; const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
@ -18,7 +19,6 @@ type Props = {
desc?: string; desc?: string;
content?: string; content?: string;
largedesc?: string; largedesc?: string;
showBreadcrumb?: boolean;
titleSize?: "large" | "medium" | string; titleSize?: "large" | "medium" | string;
backgroundImage: string | string[]; backgroundImage: string | string[];
}; };
@ -29,7 +29,6 @@ export default function Banner({
desc, desc,
content, content,
largedesc, largedesc,
showBreadcrumb = true,
titleSize = "large", titleSize = "large",
backgroundImage, backgroundImage,
}: Props) { }: Props) {
@ -42,6 +41,8 @@ export default function Banner({
const isCarousel = images.length > 1; const isCarousel = images.length > 1;
const descText = desc ?? content; const descText = desc ?? content;
const notShowBreadcrumbPaths = ["/"];
const breadcrumbItems = useMemo(() => { const breadcrumbItems = useMemo(() => {
const segments = location.pathname.split("/").filter((s) => s !== ""); const segments = location.pathname.split("/").filter((s) => s !== "");
if (segments.length === 0) { if (segments.length === 0) {
@ -52,11 +53,11 @@ export default function Banner({
paths.push((paths[i - 1] ?? "") + "/" + segments[i]); paths.push((paths[i - 1] ?? "") + "/" + segments[i]);
} }
const getLabelByPath = (path: string): string => { const getLabelByPath = (path: string): string => {
if (path === "/") return navItems.find((n) => n.index)?.label ?? "首页"; if (path === "/") return navItems.find((n: NavItem) => n.path === "/")?.label ?? "首页";
const top = navItems.find((n) => n.path === path); const top = navItems.find((n: NavItem) => n.path === path);
if (top) return top.label; if (top) return top.label;
for (const item of navItems) { for (const item of navItems) {
const child = item.children?.find((c) => c.path === path); const child = item.children?.find((c: NavChild) => c.path === path);
if (child) return child.label; if (child) return child.label;
} }
const last = path.split("/").pop() ?? path; const last = path.split("/").pop() ?? path;
@ -67,7 +68,7 @@ export default function Banner({
to: path, to: path,
})); }));
items.unshift({ items.unshift({
label: navItems.find((n) => n.index)?.label ?? "首页", label: navItems.find((n: NavItem) => n.path === "/")?.label ?? "首页",
to: "/", to: "/",
}); });
return items; return items;
@ -75,19 +76,29 @@ export default function Banner({
const heroContent = ( const heroContent = (
<div className={styles.heroContent} style={{ gap: "30px" }}> <div className={styles.heroContent} style={{ gap: "30px" }}>
<h1 className={`${styles.heroTitle} ${titleSize === "medium" ? styles.heroTitleMedium : ""}`}>{title}</h1> <ScrollReveal preset="slideUp">
{subtitle && <h2 className={styles.heroSubtitle}>{subtitle}</h2>} <h1 className={`${styles.heroTitle} ${titleSize === "medium" ? styles.heroTitleMedium : ""}`}>{title}</h1>
{descText && <p className={styles.heroDesc}>{descText}</p>} </ScrollReveal>
{largedesc && <p className={styles.heroLargeDesc}>{largedesc}</p>} {subtitle && <ScrollReveal preset="slideUp" delay={0.2}>
<div className={styles.breadcrumb}> <h2 className={styles.heroSubtitle}>{subtitle}</h2>
{showBreadcrumb && </ScrollReveal>}
(breadcrumbItems ?? []).map((item, i) => ( {descText && <ScrollReveal preset="slideUp" delay={0.2}>
<span key={i}> <p className={styles.heroDesc}>{descText}</p>
{i > 0 && <span>{" > "}</span>} </ScrollReveal>}
{item.to ? <Link to={item.to}>{item.label}</Link> : <span>{item.label}</span>} {largedesc && <ScrollReveal preset="slideUp" delay={0.2}>
</span> <p className={styles.heroLargeDesc}>{largedesc}</p>
))} </ScrollReveal>}
</div> <ScrollReveal preset="fadeIn" delay={0.2}>
<div className={styles.breadcrumb}>
{ !notShowBreadcrumbPaths.includes(location.pathname) &&
(breadcrumbItems ?? []).map((item, i) => (
<span key={i}>
{i > 0 && <span>{" > "}</span>}
{item.to ? <Link to={item.to}>{item.label}</Link> : <span>{item.label}</span>}
</span>
))}
</div>
</ScrollReveal>
</div> </div>
); );
return ( return (

View File

@ -4,7 +4,8 @@ type Data = {
title: string; title: string;
content: string; content: string;
backgroundImage: string; backgroundImage: string;
link: string; path: string;
moreText: string;
} }
export default function AnimateTopCard({ data }: { data: Data }) { export default function AnimateTopCard({ data }: { data: Data }) {
@ -16,7 +17,7 @@ export default function AnimateTopCard({ data }: { data: Data }) {
<div className={styles.cardTitleUnderline}></div> <div className={styles.cardTitleUnderline}></div>
<div className={styles.cardContent}> <div className={styles.cardContent}>
<div>{data.content}</div> <div>{data.content}</div>
<Link to={data.link} className={styles.cardMore}></Link> <Link to={data.path} className={styles.cardMore}>{data.moreText}</Link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ import styles from "./index.module.css";
type Data = { type Data = {
title: string; title: string;
createTime: string; createTime: string;
readTimes: string; readTimes: string | number;
content: string; content: string;
} }

View File

@ -1,3 +1,8 @@
/* 定义 */
:root {
--duration: 0.8s;
}
.columnXGrids { .columnXGrids {
/* 3列 */ /* 3列 */
display: grid; display: grid;
@ -7,12 +12,23 @@
.columnXGridsItem { .columnXGridsItem {
color: #fff; color: #fff;
aspect-ratio: 446/430; aspect-ratio: 446/430;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
.bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: transform;
}
&:hover .bg {
transform: scale(1.1);
}
.mask { .mask {
position: absolute; position: absolute;
top: 0; top: 0;
@ -28,7 +44,7 @@
height: 100%; height: 100%;
z-index: 2; z-index: 2;
padding: 60px 40px; padding: 60px 40px;
transition: background 0.3s ease; transition: background var(--duration) ease;
&:hover { &:hover {
background: rgba(20,53,92,0.7); background: rgba(20,53,92,0.7);
@ -51,7 +67,7 @@
color: #FFFFFF; color: #FFFFFF;
line-height: 34px; line-height: 34px;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity var(--duration) ease;
} }
} }

View File

@ -12,7 +12,8 @@ export default function ColumnXGrids({ items }: Props) {
return ( return (
<div className={styles.columnXGrids}> <div className={styles.columnXGrids}>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={index} className={styles.columnXGridsItem} style={{ backgroundImage: `url(${item.backgroundImage})` }}> <div key={index} className={styles.columnXGridsItem}>
<div className={styles.bg} style={{ backgroundImage: `url(${item.backgroundImage})` }} />
<div className={styles.mask}></div> <div className={styles.mask}></div>
<div className={styles.columnXGridsItemInner}> <div className={styles.columnXGridsItemInner}>
<div className={styles.columnXGridsItemTitle}>{item.title}</div> <div className={styles.columnXGridsItemTitle}>{item.title}</div>

View File

@ -44,4 +44,26 @@
font-size: 20px; font-size: 20px;
padding: 0 160px; padding: 0 160px;
} }
.jobPageContentTitle {
font-family: Source Han Sans, Source Han Sans;
font-weight: 600;
font-size: 24px;
color: #222222;
line-height: 34px;
text-align: left;
font-style: normal;
text-transform: none;
}
.jobPageContentText {
font-family: Source Han Sans, Source Han Sans;
font-weight: 400;
font-size: 18px;
color: #333333;
line-height: 26px;
text-align: left;
font-style: normal;
text-transform: none;
}
} }

View File

@ -9,6 +9,8 @@ type Data = {
recruitNumber: string; recruitNumber: string;
jobLocation: string; jobLocation: string;
content: string; content: string;
requirement: string;
contact: string;
} }
export default function JobPage({ data }: { data: Data }) { export default function JobPage({ data }: { data: Data }) {
@ -31,7 +33,16 @@ export default function JobPage({ data }: { data: Data }) {
</div> </div>
</div> </div>
<div className={styles.jobPageContent}> <div className={styles.jobPageContent}>
<div className={styles.jobPageContentText} dangerouslySetInnerHTML={{ __html: data.content }}></div> <div className={styles.jobPageContentTitle}></div>
<p className={styles.jobPageContentText} dangerouslySetInnerHTML={{ __html: data.content }}></p>
</div>
<div className={styles.jobPageContent} style={{ marginTop: "50px" }}>
<div className={styles.jobPageContentTitle}></div>
<p className={styles.jobPageContentText} dangerouslySetInnerHTML={{ __html: data.requirement }}></p>
</div>
<div className={styles.jobPageContent} style={{ marginTop: "50px" }}>
<div className={styles.jobPageContentTitle}></div>
<p className={styles.jobPageContentText}>{data.contact}</p>
</div> </div>
</div> </div>
) )

View File

@ -12,6 +12,7 @@
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 0; z-index: 0;
background-color: #0a0a0a;
} }
.rowAccordionBgLayer { .rowAccordionBgLayer {
position: absolute; position: absolute;
@ -19,7 +20,8 @@
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
transition: opacity var(--duration) ease; transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1);
will-change: opacity;
} }
.rowAccordion .headerRow, .rowAccordion .headerRow,
.rowAccordion .contentRow { .rowAccordion .contentRow {

View File

@ -1,10 +1,37 @@
// 横向手风琴组件 // 横向手风琴组件
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { motion, useInView, type Variants } from 'motion/react';
import styles from './index.module.css'; import styles from './index.module.css';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const contentItemVariants: Variants = {
hidden: { opacity: 0, x: 80 },
visible: { opacity: 1, x: 0 },
};
const FALLBACK_GRADIENT = "linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%)"; const FALLBACK_GRADIENT = "linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%)";
function smoothScrollTo(targetY: number, duration = 1200) {
const startY = window.scrollY;
const diff = targetY - startY;
if (Math.abs(diff) < 1) return;
let startTime: number | null = null;
function easeInOutCubic(t: number) {
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
}
function step(timestamp: number) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
window.scrollTo(0, startY + diff * easeInOutCubic(progress));
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
type Data = { type Data = {
title?: string; title?: string;
items: { items: {
@ -28,8 +55,18 @@ type Props = {
export default function RowAccordion({ data, placement='bottom' }: Props) { export default function RowAccordion({ data, placement='bottom' }: Props) {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const isInView = useInView(containerRef, { once: true, margin: "0px 0px -20% 0px" });
useEffect(() => {
if (isInView && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
smoothScrollTo(window.scrollY + rect.top, 1200);
}
}, [isInView]);
return ( return (
<div className={styles.rowAccordion}> <div ref={containerRef} className={styles.rowAccordion}>
<div className={styles.rowAccordionBgContainer}> <div className={styles.rowAccordionBgContainer}>
{data.items.map((item, index) => ( {data.items.map((item, index) => (
<div <div
@ -51,9 +88,20 @@ export default function RowAccordion({ data, placement='bottom' }: Props) {
} }
<div className={styles.contentRow} style={{ height: data.title ? 'calc(100% - 250px)' : '100%' }}> <div className={styles.contentRow} style={{ height: data.title ? 'calc(100% - 250px)' : '100%' }}>
{data.items.map((item, index) => ( {data.items.map((item, index) => (
<div className={`${styles.contentItem} ${activeIndex === index && styles.active}`} key={item.title} <motion.div
className={`${styles.contentItem} ${activeIndex === index && styles.active}`}
key={item.title}
style={{ justifyContent: placement === 'top' ? 'flex-start' : 'flex-end' }} style={{ justifyContent: placement === 'top' ? 'flex-start' : 'flex-end' }}
onMouseEnter={() => setActiveIndex(index)} onMouseEnter={() => setActiveIndex(index)}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={contentItemVariants}
transition={{
duration: 0.6,
delay: 0.5 + index * 0.12,
ease: [0.25, 0.46, 0.45, 0.94],
}}
> >
<div className={styles.contentItemContainer}> <div className={styles.contentItemContainer}>
<div className={styles.contentItemTitle}>{item.title}</div> <div className={styles.contentItemTitle}>{item.title}</div>
@ -75,7 +123,7 @@ export default function RowAccordion({ data, placement='bottom' }: Props) {
) )
} }
</div> </div>
</div> </motion.div>
))} ))}
</div> </div>
</div> </div>

View File

@ -1,13 +1,14 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styles from "./Footer.module.css"; import styles from "./Footer.module.css";
import { useStore } from "@/store"; import { useStore } from "@/store";
import type { NavItem, NavChild } from "@/type";
export default function Footer() { export default function Footer() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const footer = appConfig?.footer; const footer = appConfig?.footer;
const header = appConfig?.header; const header = appConfig?.header;
const footerColumns = header?.navItems?.filter((item) => !item.index) ?? []; const footerColumns = header?.navItems?.filter((item: NavItem) => item.path !== "/") ?? [];
const lowerLinks = footer?.lowerLinks ?? []; const lowerLinks = footer?.lowerLinks ?? [];
const socialIcons = footer?.socialIcons ?? []; const socialIcons = footer?.socialIcons ?? [];
const copyright = footer?.copyright ?? "版权声明©2001-{year} | 中国银泰投资有限公司"; const copyright = footer?.copyright ?? "版权声明©2001-{year} | 中国银泰投资有限公司";
@ -18,10 +19,10 @@ export default function Footer() {
<div className={styles.footerUpper}> <div className={styles.footerUpper}>
<div> <div>
<div className={styles.footerGrid}> <div className={styles.footerGrid}>
{footerColumns.map((col) => ( {footerColumns.map((col: NavItem) => (
<div key={col.path} className={styles.footerColumn}> <div key={col.path} className={styles.footerColumn}>
<div className={styles.footerTitle}>{col.label}</div> <div className={styles.footerTitle}>{col.label}</div>
{col.children?.map((link) => ( {col.children?.map((link: NavChild) => (
<Link <Link
key={link.path} key={link.path}
to={link.path} to={link.path}
@ -33,7 +34,7 @@ export default function Footer() {
</div> </div>
))} ))}
<div className={styles.footerRight}> <div className={styles.footerRight}>
{socialIcons.map((icon) => ( {socialIcons.map((icon: { src: string; alt: string }) => (
<img <img
key={icon.alt} key={icon.alt}
src={icon.src} src={icon.src}

View File

@ -17,17 +17,19 @@
transition: background 0.3s ease; transition: background 0.3s ease;
} }
.hoverMenu.header { .whiteMode.header {
background: #fff; background: #fff;
box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1); box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1);
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
} }
.hoverMenu .navLink { .whiteMode .navLink {
color: #222222; color: #222222;
} }
.hoverMenu { .whiteMode {
.searchBtn { .searchBtn {
color: #222222; color: #222222;
} }
@ -77,6 +79,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1.875rem; gap: 1.875rem;
height: 100%;
} }
.nav { .nav {
@ -110,11 +113,19 @@
.actions { .actions {
display: flex; display: flex;
height: 100%;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
margin-left: 1.875rem; margin-left: 1.875rem;
} }
.crossYline {
width: 1px;
height: 100%;
background: #FFFFFF;
transition: height 0.3s ease-in-out;
}
.langTrigger { .langTrigger {
display: flex; display: flex;
align-items: center; align-items: center;
@ -142,11 +153,17 @@
top: 100%; top: 100%;
left: 0; left: 0;
width: 100%; width: 100%;
height: 23.75rem; height: 0;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); /* box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); */
padding: 1.25rem; padding: 0;
z-index: 1000; z-index: 1000;
transition: height 0.9s ease-in-out;
&.visible {
height: 23.75rem;
padding: 1.25rem;
}
} }
.dropPanelContent { .dropPanelContent {

View File

@ -1,9 +1,10 @@
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { Dropdown } from "antd"; import { Dropdown } from "antd";
import styles from "./Header.module.css"; import styles from "./Header.module.css";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import type { NavChild, LocaleKey, SupportLocale } from "@/type"; import type { NavChild, NavItem, LocaleKey, SupportLocale } from "@/type";
import ScrollReveal from "@/components/ScrollReveal";
const DEFAULT_NAV_ITEMS: { path: string; label: string; children?: NavChild[] }[] = []; const DEFAULT_NAV_ITEMS: { path: string; label: string; children?: NavChild[] }[] = [];
@ -14,9 +15,9 @@ export default function Header() {
const setLocale = useStore((s) => s.setLocale); const setLocale = useStore((s) => s.setLocale);
const supportLocales = useStore((s) => s.supportLocales); const supportLocales = useStore((s) => s.supportLocales);
const navItems = appConfig?.header?.navItems?.filter((item) => !item.index) ?? DEFAULT_NAV_ITEMS; const navItems = appConfig?.header?.navItems?.filter((item: NavItem) => item.path !== "/") ?? DEFAULT_NAV_ITEMS;
const langMenuItems: SupportLocale[] = supportLocales || []; const langMenuItems: SupportLocale[] = supportLocales || [];
const logo = appConfig?.logo ?? "/images/logo.png"; const logo = appConfig?.company?.logo ?? "/images/logo.png";
const [activeNav, setActiveNav] = useState(""); const [activeNav, setActiveNav] = useState("");
const [showDropPanel, setShowDropPanel] = useState(false); const [showDropPanel, setShowDropPanel] = useState(false);
@ -31,38 +32,33 @@ export default function Header() {
} }
const activePanelItem = useMemo(() => { const activePanelItem = useMemo(() => {
return navItems.find((item) => item.path === activeNav)?.children || []; return navItems.find((item: NavItem) => item.path === activeNav)?.children || [];
}, [activeNav]); }, [activeNav]);
const [showWhiteMode, setShowWhiteMode] = useState(false); const [showWhiteMode, setShowWhiteMode] = useState(false);
const isDetailPage = location.pathname.includes("/detail/");
const isDetailPageRef = useRef(isDetailPage);
isDetailPageRef.current = isDetailPage;
useEffect(() => { useEffect(() => {
const path = location.pathname; if (isDetailPage) {
if (path.includes("/detail/")) {
setShowWhiteMode(true); setShowWhiteMode(true);
} else { return;
setShowWhiteMode(false);
} }
}, [location.pathname]); setShowWhiteMode(window.scrollY > 100);
}, [isDetailPage]);
// 监听滚动
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
const scrollTop = window.scrollY; if (isDetailPageRef.current) return;
if (scrollTop > 100) { setShowWhiteMode(window.scrollY > 100);
setShowWhiteMode(true);
} else {
setShowWhiteMode(false);
}
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
}; };
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
return ( return (
<header className={`${styles.header} ${(showDropPanel || showWhiteMode) ? styles.hoverMenu : ""}`} <header className={`${styles.header} ${(showDropPanel || showWhiteMode) ? styles.whiteMode : ""}`}
onMouseLeave={() => setShowDropPanel(false)} onMouseLeave={() => setShowDropPanel(false)}
> >
<div className={`header-row ${styles.headerInner}`}> <div className={`header-row ${styles.headerInner}`}>
@ -72,7 +68,7 @@ export default function Header() {
<div className={styles.headerRight}> <div className={styles.headerRight}>
<nav> <nav>
<ul className={styles.nav}> <ul className={styles.nav}>
{navItems.map((item) => ( {navItems.map((item: NavItem) => (
<li key={item.path} className={styles.navItem} > <li key={item.path} className={styles.navItem} >
<div className={styles.navLink} onMouseEnter={(e) => handleNavEnter(e, item.path)}> <div className={styles.navLink} onMouseEnter={(e) => handleNavEnter(e, item.path)}>
{item.label} {item.label}
@ -81,6 +77,7 @@ export default function Header() {
))} ))}
</ul> </ul>
</nav> </nav>
<div className={styles.crossYline}></div>
<div className={styles.actions}> <div className={styles.actions}>
<Link to="/search" className={styles.searchBtn} type="button" aria-label="搜索"> <Link to="/search" className={styles.searchBtn} type="button" aria-label="搜索">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26"><g><path d="M16.739973374999998,17.76985499081421C15.429279375,18.85114099081421,13.749150275,19.50064899081421,11.917317875,19.50064899081421C7.729150975,19.50064899081421,4.333984375,16.105480990814208,4.333984375,11.91731549081421C4.333984375,7.729148590814209,7.729150975,4.333981990814209,11.917317875,4.333981990814209C16.105485375,4.333981990814209,19.500651375,7.729148590814209,19.500651375,11.91731549081421C19.500651375,13.80559489081421,18.810497375,15.532677990814209,17.668674375000002,16.86007799081421L21.741985375,20.93338899081421C21.953518375,21.144921990814208,21.953518375,21.48788599081421,21.741983375,21.69941999081421L21.588777375,21.852625990814207C21.377243375,22.06416099081421,21.034278375,22.06416099081421,20.822744375,21.852625990814207L16.739973374999998,17.76985499081421ZM18.199479375,11.916142890814209C18.199479375,15.38633899081421,15.386343375,18.199475990814207,11.916145775,18.199475990814207C8.445947675,18.199475990814207,5.632812475,15.38633899081421,5.632812475,11.916142890814209C5.632812475,8.44594479081421,8.445947675,5.632810090814209,11.916145775,5.632810090814209C15.386343375,5.632810090814209,18.199479375,8.44594479081421,18.199479375,11.916142890814209Z" fillRule="evenodd" fill="#FFFFFF" fillOpacity="1" /></g></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1" width="26" height="26" viewBox="0 0 26 26"><g><path d="M16.739973374999998,17.76985499081421C15.429279375,18.85114099081421,13.749150275,19.50064899081421,11.917317875,19.50064899081421C7.729150975,19.50064899081421,4.333984375,16.105480990814208,4.333984375,11.91731549081421C4.333984375,7.729148590814209,7.729150975,4.333981990814209,11.917317875,4.333981990814209C16.105485375,4.333981990814209,19.500651375,7.729148590814209,19.500651375,11.91731549081421C19.500651375,13.80559489081421,18.810497375,15.532677990814209,17.668674375000002,16.86007799081421L21.741985375,20.93338899081421C21.953518375,21.144921990814208,21.953518375,21.48788599081421,21.741983375,21.69941999081421L21.588777375,21.852625990814207C21.377243375,22.06416099081421,21.034278375,22.06416099081421,20.822744375,21.852625990814207L16.739973374999998,17.76985499081421ZM18.199479375,11.916142890814209C18.199479375,15.38633899081421,15.386343375,18.199475990814207,11.916145775,18.199475990814207C8.445947675,18.199475990814207,5.632812475,15.38633899081421,5.632812475,11.916142890814209C5.632812475,8.44594479081421,8.445947675,5.632810090814209,11.916145775,5.632810090814209C15.386343375,5.632810090814209,18.199479375,8.44594479081421,18.199479375,11.916142890814209Z" fillRule="evenodd" fill="#FFFFFF" fillOpacity="1" /></g></svg>
@ -106,14 +103,31 @@ export default function Header() {
</div> </div>
</div> </div>
{showDropPanel && activePanelItem.length > 0 && <DropPanel items={activePanelItem} left={hoverElLeft} onLinkClick={() => setShowDropPanel(false)} />} <DropPanel items={activePanelItem} left={hoverElLeft}
onLinkClick={() => setShowDropPanel(false)}
show={showDropPanel && activePanelItem.length > 0}
/>
</header> </header>
); );
} }
function DropPanel({ items, left, onLinkClick }: { items: NavChild[]; left: number, onLinkClick: () => void }) { function DropPanel({ items, left, onLinkClick, show }: {
items: NavChild[];
left: number,
onLinkClick: () => void
show: boolean
}) {
const [visible, setVisible] = useState(show);
useEffect(() => {
const timeout = setTimeout(() => {
setVisible(show);
}, 100);
return () => {
clearTimeout(timeout);
}
}, [show]);
if (!visible) return null;
return ( return (
<div id="drop-panel" className={styles.dropPanel} style={{ paddingLeft: left, }}> <div id="drop-panel" className={`${styles.dropPanel} ${visible ? styles.visible : ""}`} style={{ paddingLeft: left}}>
<div className={styles.dropPanelContent}> <div className={styles.dropPanelContent}>
{items.map((item) => ( {items.map((item) => (
<div key={item.path} className={styles.dropPanelItem}> <div key={item.path} className={styles.dropPanelItem}>

View File

@ -23,14 +23,13 @@ export default function AboutFounder() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
subtitle={banner?.subtitle} subtitle={banner?.subtitle}
desc={banner?.content} desc={banner?.content}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
{section1Data && ( {section1Data && (
<ParagraphSection data={section1Data}> <ParagraphSection data={section1Data}>
<div className={styles.images}> <div className={styles.images}>
{section1Data.items?.map((item, index) => ( {section1Data.items?.map((item: { title: string; content?: string; backgroundImage?: string }, index: number) => (
<div className={styles.imageItem} key={item.title}> <div className={styles.imageItem} key={item.title}>
<img src={item.backgroundImage} alt={item.title} /> <img src={item.backgroundImage} alt={item.title} />
<div className={styles.imageMask} /> <div className={styles.imageMask} />
@ -73,9 +72,9 @@ export default function AboutFounder() {
<Section title={section3Data?.title} titleColor="#fff" background={section3Data?.backgroundImage}> <Section title={section3Data?.title} titleColor="#fff" background={section3Data?.backgroundImage}>
<div className={styles.section3Content}> <div className={styles.section3Content}>
{Array.from({ {Array.from({
length: Math.max(0, ...(section3Data?.columns?.map((c) => c.length) ?? [0])), length: Math.max(0, ...(section3Data?.columns?.map((c: { title?: string }[]) => c.length) ?? [0])),
}).flatMap((_, rowIndex) => }).flatMap((_, rowIndex) =>
(section3Data?.columns ?? []).map((colItems, colIndex) => ( (section3Data?.columns ?? []).map((colItems: { title?: string }[], colIndex: number) => (
<div key={`${rowIndex}-${colIndex}`} className={styles.section3Item}> <div key={`${rowIndex}-${colIndex}`} className={styles.section3Item}>
{colItems[rowIndex] ? <li>{colItems[rowIndex].title}</li> : null} {colItems[rowIndex] ? <li>{colItems[rowIndex].title}</li> : null}
</div> </div>

View File

@ -49,6 +49,7 @@
align-items: flex-start; align-items: flex-start;
min-height: 6rem; min-height: 6rem;
margin-bottom: 1rem; margin-bottom: 1rem;
scroll-margin-top: 120px;
} }
.timelineItem .side { .timelineItem .side {
@ -123,6 +124,7 @@
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
background: #14355C; background: #14355C;
transition: all 0.3s ease-in-out;
} }
.dotWrapper.selected::before { .dotWrapper.selected::before {
@ -136,6 +138,7 @@
border: 2px solid #14355C; border: 2px solid #14355C;
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
transition: all 0.3s ease-in-out;
} }
.dotLine { .dotLine {
@ -146,8 +149,10 @@
width: 0.375rem; width: 0.375rem;
min-height: 0; min-height: 0;
background: #14355C; background: #14355C;
transition: all 0.9s ease-in-out;
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.section { .section {
padding: 4rem 4rem; padding: 4rem 4rem;

View File

@ -1,19 +1,18 @@
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
import YearPicker from "@/components/YearPicker"; import YearPicker from "@/components/YearPicker";
import styles from "./History.module.css"; import styles from "./History.module.css";
import { useState, useRef, useLayoutEffect } from "react"; import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import appApi from "@/api/app";
type TimelineItem = { year: number; content: string }; type TimelineItem = { year: number; content: string, lang: string };
export default function AboutHistory() { export default function AboutHistory() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const locale = useStore((s) => s.locale);
const history = appConfig?.about?.history; const history = appConfig?.about?.history;
const banner = history?.banner; const banner = history?.banner;
const section1Data = (history?.section1Data ?? []) as TimelineItem[];
const [year, setYear] = useState<number | null>(null); const [year, setYear] = useState<number | null>(null);
const handleYearChange = (year: number | null) => { const handleYearChange = (year: number | null) => {
setYear(year); setYear(year);
const yearEl = document.querySelector(`#year-${year}`); const yearEl = document.querySelector(`#year-${year}`);
@ -22,6 +21,25 @@ export default function AboutHistory() {
} }
}; };
const [processList, setProcessList] = useState<TimelineItem[]>([]);
const localProcessList = useMemo(() => {
return processList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
}, [processList, locale]);
// 接口数据
useEffect(() => {
appApi.getProcessList().then((res) => {
const items = res.data.items.map((item:any) => {
return {
year: item.title,
content: item.content,
lang: item.lang,
}
})
setProcessList(items);
});
}, [])
if (!history) return null; if (!history) return null;
return ( return (
@ -29,7 +47,6 @@ export default function AboutHistory() {
<Banner <Banner
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -38,7 +55,7 @@ export default function AboutHistory() {
<YearPicker placeholder="年份" value={year} onChange={handleYearChange} /> <YearPicker placeholder="年份" value={year} onChange={handleYearChange} />
</div> </div>
<HistoryTimeline <HistoryTimeline
data={section1Data} data={localProcessList}
selectedYear={year} selectedYear={year}
onYearChange={setYear} onYearChange={setYear}
/> />

View File

@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
import styles from "./About.module.css"; import styles from "./About.module.css";
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
import { useStore } from "@/store"; import { useStore } from "@/store";
import ScrollReveal from "@/components/ScrollReveal";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)"; const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
@ -10,7 +11,7 @@ export default function About() {
const overview = appConfig?.about?.overview; const overview = appConfig?.about?.overview;
const banner = overview?.banner; const banner = overview?.banner;
const section1Data = overview?.section1Data ?? []; const section1Data = overview?.section1Data?.items ?? [];
if (!overview) return null; if (!overview) return null;
@ -19,11 +20,10 @@ export default function About() {
<Banner <Banner
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
{section1Data.map((item, index) => ( {section1Data.map((item: { title: string; content?: string; backgroundImage?: string; links?: { label: string; path: string }[] }, index: number) => (
<section <section
key={index} key={index}
className={`${styles.section} ${index % 2 === 1 ? styles.sectionRight : ""}`} className={`${styles.section} ${index % 2 === 1 ? styles.sectionRight : ""}`}
@ -34,17 +34,19 @@ export default function About() {
<div <div
className={`${styles.content} ${index % 2 === 1 ? styles.contentRight : ""}`} className={`${styles.content} ${index % 2 === 1 ? styles.contentRight : ""}`}
> >
<div className={styles.textBox}> <ScrollReveal preset="slideUp">
<h2 className={styles.title}>{item.title}</h2> <div className={styles.textBox}>
<p className={styles.desc}>{item.content}</p> <h2 className={styles.title}>{item.title}</h2>
<div className={styles.links}> <p className={styles.desc}>{item.content}</p>
{item.links?.map((link) => ( <div className={styles.links}>
<span key={link.label}> {item.links?.map((link: { label: string; path: string }) => (
<Link to={link.to}>{link.label}</Link> <span key={link.label}>
</span> <Link to={link.path}>{link.label}</Link>
))} </span>
))}
</div>
</div> </div>
</div> </ScrollReveal>
</div> </div>
</section> </section>
))} ))}

View File

@ -43,7 +43,6 @@ export default function BaseGroup() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -34,7 +34,6 @@ export default function BusinessCommercialGroup() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"}
/> />

View File

@ -20,7 +20,6 @@ export default function BusinessCommercialGroupDetail() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
largedesc={(banner as BannerConfig)?.largeContent} largedesc={(banner as BannerConfig)?.largeContent}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"}
/> />

View File

@ -23,7 +23,6 @@ export default function InvestGroup() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -39,7 +39,6 @@ export default function RealtyGroup() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -36,7 +36,6 @@ export default function RuijingGroup() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -260,13 +260,13 @@
.newsFeatured { .newsFeatured {
overflow: hidden; overflow: hidden;
cursor: pointer;
} }
.newsFeaturedImgWrap { .newsFeaturedImgWrap {
width: 59.375rem; width: 59.375rem;
aspect-ratio: 950 / 560; aspect-ratio: 950 / 560;
background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%); background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%);
background: red;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
@ -294,6 +294,12 @@
gap: 0.625rem; gap: 0.625rem;
} }
.newsItemDateWrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.newsItem { .newsItem {
box-sizing: border-box; box-sizing: border-box;
padding: 1.5rem 1.875rem 1.25rem 1.875rem; padding: 1.5rem 1.875rem 1.25rem 1.875rem;
@ -302,15 +308,36 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
cursor: pointer;
.newsItemArrowIcon {
transition: transform 0.3s ease;
}
&.active {
.newsItemTitle {
color: #14355C;
font-weight: 600;
}
.newsItemSnippet {
color: #333;
}
.newsItemArrowIcon {
color: #14355C;
}
}
} }
.newsItemTitle { .newsItemTitle {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 500; font-weight: 500;
color: #333; color: #222222;
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
line-height: 1.4; line-height: 1.4;
transition: color 0.3s ease-in-out;
} }
.newsItemTitle a { .newsItemTitle a {
@ -327,6 +354,7 @@
color: #666; color: #666;
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
line-height: 1.6; line-height: 1.6;
height: 75px;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
@ -339,3 +367,7 @@
font-size: 0.875rem; font-size: 0.875rem;
color: #999; color: #999;
} }

View File

@ -1,9 +1,18 @@
import { useStore } from "@/store"; import { useStore } from "@/store";
import { Link } from "react-router-dom";
import styles from "./Home.module.css"; import styles from "./Home.module.css";
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
import Section from "@/components/layout/Section"; import Section from "@/components/layout/Section";
import RowAccordion from "@/components/layout/RowAccordion"; import RowAccordion from "@/components/layout/RowAccordion";
import { Link } from "react-router-dom"; import ScreenOpen from "@/components/ScreenOpen";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { motion, type Variants } from "motion/react";
import { ArrowRightOutlined } from "@ant-design/icons";
import appApi from "@/api/app";
const cardVariants: Variants = {
hidden: { opacity: 0, y: 220 },
visible: { opacity: 1, y: 0 },
};
export default function Home() { export default function Home() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
@ -11,76 +20,168 @@ export default function Home() {
const banner = home?.banner; const banner = home?.banner;
const section1Data = home?.section1Data; const section1Data = home?.section1Data;
const section2Data = home?.section2Data ?? []; const section2Data = home?.section2Data?.items ?? [];
const newsData = home?.newsData ?? [];
if (!home) return null; if (!home) return null;
return ( return (
<div> <div>
{/* 开屏动画 */}
<ScreenOpen />
{/* Hero */} {/* Hero */}
<Banner <Banner
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
{/* Commercial */} {/* Commercial */}
{section1Data && <RowAccordion data={section1Data} />} {section1Data &&
<RowAccordion data={section1Data} />
}
{/* 首页名片 */} {/* 首页名片 */}
<Section background="/images/bg-mask-card.png"> <Section background="/images/bg-mask-card.png">
<div className={styles.cardsInner}> <div className={styles.cardsInner}>
{section2Data.map((item, i) => ( {section2Data.map((item: any, i: number) => (
<div key={i} className={`${styles.card} ${styles.cardSustainable}`}> <motion.div
<div key={i}
className={styles.cardBg} initial="hidden"
style={{ whileInView="visible"
backgroundImage: `url(${item.backgroundImage})`, viewport={{ once: true, margin: "0px 0px -30% 0px" }}
}} variants={cardVariants}
/> transition={{
<div className={styles.cardOverlay} /> duration: 1.5,
<div className={styles.cardContent}> delay: i * 0.15,
<h3 className={styles.cardTitle}>{item.title}</h3> ease: [0.25, 0.46, 0.45, 0.94],
<p className={styles.cardDesc}>{item.content}</p> }}
</div> >
</div> <Link to={item.path} className={`${styles.card} ${styles.cardSustainable} hover-scale`}>
<div
className={`${styles.cardBg} hover-scale-bg`}
style={{
backgroundImage: `url(${item.backgroundImage})`,
}}
/>
<div className={styles.cardOverlay} />
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{item.title}</h3>
<p className={styles.cardDesc}>{item.content}</p>
</div>
</Link>
</motion.div>
))} ))}
</div> </div>
</Section> </Section>
{/* News 动态数据 */} <News />
<Section title="新闻资讯" maskBackground="#F0F2F4">
<div className={styles.newsGrid}>
<div className={styles.newsFeatured}>
<div className={styles.newsFeaturedImgWrap}>
<img
src="/images/bg-overview.png"
alt="新闻配图"
className={styles.newsFeaturedImg}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<p className={styles.newsFeaturedCaption}>
</p>
</div>
</div>
<div className={styles.newsList}>
{newsData.map((item) => (
<article key={item.path} className={styles.newsItem}>
<h3 className={styles.newsItemTitle}>
<Link to={item.path}>{item.title}</Link>
</h3>
<p className={styles.newsItemSnippet}>{item.snippet}</p>
<span className={styles.newsItemDate}>{item.date}</span>
</article>
))}
</div>
</div>
</Section>
</div> </div>
); );
} }
function News() {
const locale = useStore((s) => s.locale);
const [newsData, setNewsData] = useState<any[]>([]);
const videoRef = useRef<HTMLVideoElement | null>(null);
const localNewsData = useMemo(() => {
return newsData.filter((item) => item.lang.toLowerCase() === locale.split('-')[0]);
}, [newsData, locale]);
const [activeNewsIndex, setActiveNewsIndex] = useState(0);
const handleNewsEnter = (index: number) => {
setActiveNewsIndex(index);
};
const handleVideoPlay = () => {
if (videoRef.current) {
videoRef.current.play();
}
};
const handleVideoPause = () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
}
};
const handleSearch = useCallback((category_id: any) => {
appApi.getNewsList({ page: 1, size: 6, sort: "create_time DESC", category_id }).then((res) => {
const data = res.data.items.map((item: any) => {
return {
id: item.id,
title: item.title,
date: item.create_time,
image: item.covers_show === 'image' ? item.covers.image[0] : '',
video: item.covers_show === 'video' ? item.covers.video[0] : '',
snippet: item.content,
lang: item.lang,
path: '/news/detail/' + item.id,
}
})
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) => {
handleSearch(category_id)
});
}, [])
return (
<Section title="新闻资讯" maskBackground="#F0F2F4">
<div className={styles.newsGrid}>
<div className={`${styles.newsFeatured} hover-scale`}>
<div className={styles.newsFeaturedImgWrap}>
{
localNewsData[activeNewsIndex]?.image ? (
<img
src={localNewsData[activeNewsIndex]?.image}
alt="新闻配图"
className={`${styles.newsFeaturedImg} hover-scale-bg`}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : (
<video
ref={videoRef}
src={localNewsData[activeNewsIndex]?.video}
className={`${styles.newsFeaturedImg} hover-scale-bg`}
onMouseEnter={handleVideoPlay}
onMouseLeave={handleVideoPause}
></video>
)
}
<p className={styles.newsFeaturedCaption}>
</p>
</div>
</div>
<div className={styles.newsList}>
{localNewsData.map((item, index) => (
<article key={index} className={`${styles.newsItem} ${activeNewsIndex === index && styles.active}`}
onMouseEnter={() => handleNewsEnter(index)}>
<Link to={'/news/detail/' + item.id}>
<h3 className={styles.newsItemTitle}>
{item.title}
</h3>
<p className={styles.newsItemSnippet}>{item.snippet}</p>
<div className={styles.newsItemDateWrap}>
<span className={styles.newsItemDate}>{item.date}</span>
<ArrowRightOutlined className={styles.newsItemArrowIcon} style={{ fontSize: '12px' }} />
</div>
</Link>
</article>
))}
</div>
</div>
</Section>
);
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import styles from "./Campus.module.css"; import styles from "./Campus.module.css";
import Banner, { type BannerConfig } from "@/components/Banner"; import Banner, { type BannerConfig } from "@/components/Banner";
import Section from "@/components/layout/Section"; import Section from "@/components/layout/Section";
@ -8,16 +8,20 @@ import { Empty, Select } from "antd";
import Pagination from "@/components/Pagination"; import Pagination from "@/components/Pagination";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { debounce } from "@/utils"; import { debounce } from "@/utils";
import appApi from "@/api/app";
type JobItem = { type JobItem = {
id: number; id: number;
title: string; title: string;
content: string; content: string;
labels: string[]; labels: string[];
lang: string
} }
export default function JoinCampus() { export default function JoinCampus() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const supportLocales = useStore((s) => s.supportLocales);
const locale = useStore((s) => s.locale);
const data = appConfig?.join?.campus; const data = appConfig?.join?.campus;
const banner = data?.banner; const banner = data?.banner;
@ -25,37 +29,69 @@ export default function JoinCampus() {
// 职业类别 业务领域 所属板块 // 职业类别 业务领域 所属板块
const [jobType, setJobType] = useState(''); const [jobType, setJobType] = useState('');
const [jobTypeOptions, setJobTypeOptions] = useState([ const [jobTypeOptions, setJobTypeOptions] = useState([]);
{ label: "全部", value: "" },
{ label: "职业类别1", value: "1" },
{ label: "职业类别2", value: "2" },
{ label: "职业类别3", value: "3" },
]);
const [businessArea, setBusinessArea] = useState(''); const [businessArea, setBusinessArea] = useState('');
const [businessAreaOptions, setBusinessAreaOptions] = useState([]); const [businessAreaOptions, setBusinessAreaOptions] = useState([]);
const [businessPlate, setBusinessPlate] = useState(''); const [businessPlate, setBusinessPlate] = useState('');
const [businessPlateOptions, setBusinessPlateOptions] = useState([]); const [businessPlateOptions, setBusinessPlateOptions] = useState([]);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [size] = useState(9); const [size] = useState(2 * supportLocales.length);
const [total, setTotal] = useState(1000); const [total, setTotal] = useState(1000);
const [jobList, setJobList] = useState<JobItem[]>([]); const [jobList, setJobList] = useState<JobItem[]>([]);
const localJobList = useMemo(() => {
return jobList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
}, [jobList, locale]);
const refreshData = useCallback(debounce(() => { // 用 ref 保存最新参数,保证 debounce 使用稳定的函数引用
console.log('refreshData2'); const paramsRef = useRef({ searchValue, jobType, businessArea, businessPlate, page, size });
setJobList([ paramsRef.current = { searchValue, jobType, businessArea, businessPlate, page, size };
{ id: 1, title: '职位1', content: '工作职责1、为集团的各类投资项目提供法律支持包括组织外部律师进行尽职调查、审阅及起草交易文件、必要时参与法律谈判、提示法律风险等2、协助完善集团本部的规章制度和合规体系3、审阅集团各部门提交法务部审阅的日常业务法务部审阅的日常业务法务部审阅的日常业务', labels: ['标签1', '标签2'] },
{ id: 2, title: '职位2', content: '职位2内容', labels: ['标签1', '标签2'] }, const refreshData = useMemo(() => debounce(() => {
{ id: 3, title: '职位3', content: '职位3内容', labels: ['标签1', '标签2'] }, const { searchValue: sv, jobType: jt, businessArea: ba, businessPlate: bp, page: p, size: s } = paramsRef.current;
]) appApi.getJobList({ page: p, size: s, sort: "", title: sv, job_type: jt, business_area: ba, business_plate: bp }).then((res) => {
const items = res.data.items.map((item: any) => {
const { job_type_name, job_area_name, job_unit_name } = item.category;
const labels = [job_type_name, job_area_name, job_unit_name];
return {
id: item.id,
title: item.title,
content: item.description,
labels,
lang: item.lang,
};
});
setJobList(items || []);
setTotal(res.data.total);
});
}, 500), []); }, 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);
}
})
})
}, []);
useEffect(() => { useEffect(() => {
refreshData(); refreshData();
}, [searchValue, jobType, businessArea, businessPlate, page, size]); }, [searchValue, jobType, businessArea, businessPlate, page, size]);
useEffect(() => {
getTypes();
}, []);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
setSearchValue(''); setSearchValue('');
setJobType(''); setJobType('');
@ -70,7 +106,6 @@ export default function JoinCampus() {
title={banner?.title ?? "招贤纳士"} title={banner?.title ?? "招贤纳士"}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -88,21 +123,21 @@ export default function JoinCampus() {
<div className={styles.campusColRight}> <div className={styles.campusColRight}>
<div className={styles.jobList}> <div className={styles.jobList}>
{/* 没有数据时显示 */} {/* 没有数据时显示 */}
{jobList.length === 0 && ( {localJobList.length === 0 && (
<div className={styles.noData}> <div className={styles.noData}>
<Empty description="暂无数据" /> <Empty description="暂无数据" />
</div> </div>
)} )}
{jobList.map(item => ( {localJobList.map((item, index) => (
<Link to={`/join/campus/detail/${item.id}`}> <Link key={item.id} to={`/join/campus/detail/${item.id}`}>
<div key={item.id} 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}> <RightOutlined /></div>
</div> </div>
<div className={styles.jobItemLabels}> <div className={styles.jobItemLabels}>
{item.labels.map(label => ( {item.labels.map((label, index) => (
<div key={label} className={styles.jobItemLabel}>&nbsp;&nbsp;&nbsp;{label}</div> <div key={index} className={styles.jobItemLabel}>&nbsp;&nbsp;&nbsp;{label}</div>
))} ))}
</div> </div>
<div className={styles.jobItemContent}>{item.content}</div> <div className={styles.jobItemContent}>{item.content}</div>

View File

@ -1,18 +1,46 @@
import JobPage from "@/components/layout/JobPage"; import JobPage from "@/components/layout/JobPage";
import { useParams } from "react-router-dom";
import appApi from "@/api/app";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useStore } from "@/store";
export default function CampusDetail() { export default function CampusDetail() {
const data = { const locale = useStore((s) => s.locale);
title: "项目营销总监", const params = useParams();
jobType: "营销总监", const id = params.id as string;
businessArea: "营销", const [jobDetail, setJobDetail] = useState<any>(null);
businessPlate: "营销", const localJobDetail = useMemo(() => {
recruitNumber: "2", return jobDetail?.find((item: any) => item.lang.toLowerCase() === locale.split('-')[0]);
jobLocation: "杭州", }, [jobDetail, locale]);
content: "国以才立政以才治业以才兴做好新形势下立法工作保障立法质量人才队伍是关键。自2020年起在银泰公益基金会的支持下浙江立法研究院、浙江大学立法研究院联合浙江大学光华法学院致力于助推立法学科建设经过一年多时间的不懈努力获批增设立法学二级学科并面向全国招收立法学专业硕士、博士研究生旨在积极响应新时代下国家对立法学理论研究和人才培养的需求践行依法护航经济社会发展的理念不断开展探索助推立法工作更好地为中国特色社会主义法治建设服务。",
} const getJobDetail = useCallback(() => {
appApi.getJobDetail(id).then((res) => {
const items = res.data.map((item:any) => {
return {
title: item.title,
jobType: item.category.job_type_name,
businessArea: item.category.job_area_name,
businessPlate: item.category.job_unit_name,
recruitNumber: item.recruit_count,
jobLocation: item.city,
content: item.description,
requirement: item.requirement,
lang: item.lang,
contact: item.contact,
}
});
setJobDetail(items);
})
}, [id])
useEffect(() => {
getJobDetail()
}, [])
return ( return (
<div> <div>
<JobPage data={data} /> {
localJobDetail &&
<JobPage data={localJobDetail} />
}
</div> </div>
) )
} }

View File

@ -19,7 +19,6 @@ export default function Culture() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -30,7 +29,7 @@ export default function Culture() {
maskBackground="#D8D8D8" maskBackground="#D8D8D8"
> >
<div className={styles.cultureItems}> <div className={styles.cultureItems}>
{section1Data.items?.map((item, index) => ( {section1Data.items?.map((item: any, index: number) => (
<div <div
key={index} key={index}
className={styles.cultureItem} className={styles.cultureItem}
@ -54,7 +53,7 @@ export default function Culture() {
maskBackground="rgba(216,216,216,0.6)" maskBackground="rgba(216,216,216,0.6)"
> >
<div className={styles.valuesItems}> <div className={styles.valuesItems}>
{section2Data.items?.map((item, index) => ( {section2Data.items?.map((item: { title: string; content?: string; icon?: string }, index: number) => (
<div key={index} className={styles.valuesItem}> <div key={index} className={styles.valuesItem}>
<div <div
className={styles.valuesItemIcon} className={styles.valuesItemIcon}

View File

@ -1,26 +1,48 @@
import Article from "@/components/layout/Article"; import Article from "@/components/layout/Article";
import { useStore } from "@/store"; import { useStore } from "@/store";
import { useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import appApi from "@/api/app";
type NewsDetail = {
id: number;
title: string;
createTime: string;
readTimes: number;
content: string;
lang: "lang" | "EN"
}
export default function NewsDetail() { export default function NewsDetail() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const data = appConfig?.news?.detail; const locale = useStore((s) => s.locale);
const params = useParams();
const id = params.id as string;
const [newsDetailList, setNewsDetailList] = useState<NewsDetail[]>();
const newsDetail = useMemo(() => {
return newsDetailList?.find((item) => item.lang.toLowerCase() === locale.split('-')[0]);
}, [newsDetailList, locale]);
if (!data) return null; useEffect(() => {
appApi.getNewsDetail(id).then((res) => {
const data = res.data.map((item: any) => {
return {
id: item.id,
title: item.title,
createTime: item.create_time,
readTimes: item.count,
content: item.content,
lang: item.lang,
}
})
setNewsDetailList(data)
})
}, [id])
const section1Data = data.section1Data;
const articleData = section1Data
? {
title: section1Data.title,
createTime: section1Data.createTime,
readTimes: section1Data.readTimes,
content: section1Data.content,
}
: null;
return ( return (
<div> <div>
{articleData && <Article data={articleData} />} {newsDetail && <Article data={newsDetail} />}
</div> </div>
); );
} }

View File

@ -19,13 +19,12 @@ export default function Media() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
<Section background="/images/bg-overview.png" maskBackground="rgba(1,11,72,0.8)"> <Section background="/images/bg-overview.png" maskBackground="rgba(1,11,72,0.8)">
<div className={styles.mediaItems}> <div className={styles.mediaItems}>
{items.map((item, index) => ( {items.map((item: { title: string; content?: string }, index: number) => (
<div key={index} className={styles.mediaItem}> <div key={index} className={styles.mediaItem}>
<div className={styles.mediaItemTitle}>{item.title}</div> <div className={styles.mediaItemTitle}>{item.title}</div>
<div className={styles.mediaItemContent}>{item.content}</div> <div className={styles.mediaItemContent}>{item.content}</div>

View File

@ -39,6 +39,7 @@
.newList { .newList {
/* 3列 */ /* 3列 */
min-height: 200px;
margin-top: 100px; margin-top: 100px;
margin-bottom: 100px; margin-bottom: 100px;
display: grid; display: grid;
@ -53,12 +54,13 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
img { img, video {
width: 100%; width: 100%;
/* height: 300px; */ /* height: 300px; */
aspect-ratio: 452 / 300; aspect-ratio: 452 / 300;
object-fit: cover; object-fit: cover;
cursor: pointer; cursor: pointer;
transition: all 0.5s ease;
} }
&:hover { &:hover {
@ -70,6 +72,10 @@
.newItemCreateTime { .newItemCreateTime {
color: #fff !important; color: #fff !important;
} }
img {
transform: scale(1.05);
}
} }
.newItemContent { .newItemContent {

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import styles from "./NewsPublic.module.css"; import styles from "./NewsPublic.module.css";
import Banner, { type BannerConfig } from "@/components/Banner"; import Banner, { type BannerConfig } from "@/components/Banner";
import Section from "@/components/layout/Section"; import Section from "@/components/layout/Section";
@ -6,27 +6,69 @@ import { SearchOutlined } from "@ant-design/icons";
import Pagination from "@/components/Pagination"; import Pagination from "@/components/Pagination";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useStore } from "@/store"; import { useStore } from "@/store";
import appApi from "@/api/app";
type NewsItem = {
id: number;
title: string;
createTime: string;
image: string;
video: string;
lang: "ZH" | "EN"
}
export default function NewsPublic() { export default function NewsPublic() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const locale = useStore((s) => s.locale);
const supportLocales = useStore((s) => s.supportLocales);
const data = appConfig?.news?.public; const data = appConfig?.news?.public;
const [newList] = useState([ const [page, setPage] = useState(1);
{ id: 1, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" }, const [size] = useState(9 * supportLocales.length);
{ id: 2, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" }, const [total, setTotal] = useState(0);
{ id: 3, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" }, const categoryIdRef = useRef<string | null>('');
]);
const [newList, setNewList] = useState<NewsItem[]>([]);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
console.log("search", searchValue); appApi.getNewsList({ page, size, sort: "create_time DESC", title: searchValue,
}, [searchValue]); category_id: categoryIdRef.current ?? '',
}).then((res) => {
const data = res.data.items.map((item:any) => {
return {
id: item.id,
title: item.title,
createTime: item.create_time,
image: item.covers_show === 'image' ? item.covers.image[0] : '',
video: item.covers_show === 'video' ? item.covers.video[0] : '',
lang: item.lang,
}
})
setNewList(data);
setTotal(res.data.total);
});
}, [searchValue, page, size]);
const [page, setPage] = useState(1); const localNewsList = useMemo(() => {
const [size] = useState(9); return newList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
const [total, setTotal] = useState(1000); }, [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; const banner = data?.banner;
useEffect(() => {
getCategoryList().then(() => {
handleSearch();
})
}, [page, size]);
if (!data) return null; if (!data) return null;
return ( return (
@ -35,7 +77,6 @@ export default function NewsPublic() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -56,13 +97,33 @@ export default function NewsPublic() {
</div> </div>
<div className={styles.newList}> <div className={styles.newList}>
{newList.map((item, index) => ( {localNewsList.map((item, index) => (
<Link <Link
key={index} key={index}
to={`/news/detail/${item.id}`} to={`/news/detail/${item.id}`}
className={styles.newItem} className={styles.newItem}
> >
<img src={item.image} alt={item.title} /> {item.image ? (
<img src={item.image} alt={item.title} />
) : (
<video
ref={(el) => {
videoRefs.current[index] = el;
}}
src={item.video}
onMouseEnter={() => {
const v = videoRefs.current[index];
v?.play();
}}
onMouseLeave={() => {
const v = videoRefs.current[index];
if (v) {
v.pause();
v.currentTime = 0;
}
}}
/>
)}
<div className={styles.newItemContent}> <div className={styles.newItemContent}>
<div className={styles.newItemTitle}>{item.title}</div> <div className={styles.newItemTitle}>{item.title}</div>
<div className={styles.newItemCreateTime}>{item.createTime}</div> <div className={styles.newItemCreateTime}>{item.createTime}</div>

View File

@ -21,7 +21,6 @@ export default function AuditReport() {
title={banner?.title ?? "审计举报"} title={banner?.title ?? "审计举报"}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -19,7 +19,6 @@ export default function PrivacyPolicy() {
title={banner?.title ?? "隐私保护"} title={banner?.title ?? "隐私保护"}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -48,7 +48,6 @@ export default function Search() {
title={banner?.title ?? "搜索结果"} title={banner?.title ?? "搜索结果"}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/search-banner.png"} backgroundImage={banner?.backgroundImage ?? "/images/search-banner.png"}
/> />

View File

@ -21,7 +21,6 @@ export default function SiteMap() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -30,7 +29,7 @@ export default function SiteMap() {
<div className={styles.siteMapTitle}>{title}</div> <div className={styles.siteMapTitle}>{title}</div>
<div className={styles.siteMapItems}> <div className={styles.siteMapItems}>
{items.map((item) => ( {items.map((item: { label: string; path: string; children?: { path: string; label: string; children?: { path: string; label: string }[] }[] }) => (
<div className={styles.siteMapItem} key={item.label}> <div className={styles.siteMapItem} key={item.label}>
<div className={styles.siteMapItemLabel}>{item.label}</div> <div className={styles.siteMapItemLabel}>{item.label}</div>
<div className={styles.siteMapItemChildren}> <div className={styles.siteMapItemChildren}>

View File

@ -19,7 +19,6 @@ export default function TermsOfUse() {
title={banner?.title ?? "使用条款"} title={banner?.title ?? "使用条款"}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"} titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -44,7 +44,6 @@ export default function PropertyService() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
desc={banner?.content} desc={banner?.content}
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"} titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />

View File

@ -27,7 +27,6 @@ export default function Foundation() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "medium"} titleSize={(banner as BannerConfig)?.titleSize ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -44,13 +43,13 @@ export default function Foundation() {
maskBackground="rgba(255,255,255,0.3)" maskBackground="rgba(255,255,255,0.3)"
> >
<div className={styles.publicWelfareDataItems}> <div className={styles.publicWelfareDataItems}>
{section2Data.items?.map((item, index) => ( {section2Data.items?.map((item: { title: string; content?: string; backgroundImage?: string; path?: string; moreText?: string }, index: number) => (
<div <div
key={index} key={index}
className={styles.publicWelfareDataItem} className={styles.publicWelfareDataItem}
style={{ backgroundImage: `url(${item.backgroundImage})` }} style={{ backgroundImage: `url(${item.backgroundImage})` }}
> >
<AnimateTopCard data={item} /> <AnimateTopCard data={{ ...item, content: item.content ?? "", path: item.path ?? "", moreText: item.moreText ?? "", backgroundImage: item.backgroundImage ?? "" }} />
</div> </div>
))} ))}
</div> </div>
@ -67,7 +66,7 @@ export default function Foundation() {
> >
<div className={styles.informationPublicDataContent}> <div className={styles.informationPublicDataContent}>
<div className={styles.informationPublicDataItems}> <div className={styles.informationPublicDataItems}>
{section3Data.tabItems?.[activeIndex]?.fileItems?.map((item, index) => ( {section3Data.tabItems?.[activeIndex]?.fileItems?.map((item: any, index: number) => (
<div key={index} className={styles.informationPublicDataItem}> <div key={index} className={styles.informationPublicDataItem}>
<li> <li>
<span>{item.fileName}</span> <span>{item.fileName}</span>
@ -87,7 +86,7 @@ export default function Foundation() {
{section4Data && ( {section4Data && (
<Section title={section4Data.title} background="" maskBackground="#F7FBFF"> <Section title={section4Data.title} background="" maskBackground="#F7FBFF">
<div className={styles.partnerItems}> <div className={styles.partnerItems}>
{section4Data.items?.map((item, index) => ( {section4Data.items?.map((item: any, index: number) => (
<div key={index} className={styles.partnerItem}> <div key={index} className={styles.partnerItem}>
<img src={item.logo} alt="logo" /> <img src={item.logo} alt="logo" />
</div> </div>

View File

@ -16,7 +16,7 @@ export default function Sustainability() {
const banner = data.banner; const banner = data.banner;
const section1Data = data.section1Data; const section1Data = data.section1Data;
const section2Data = data.section2Data ?? []; const section2Data = data.section2Data?.items ?? [];
const section3Data = data.section3Data; const section3Data = data.section3Data;
const section4Data = data.section4Data; const section4Data = data.section4Data;
@ -28,7 +28,6 @@ export default function Sustainability() {
title={banner?.title ?? ""} title={banner?.title ?? ""}
content={(banner as BannerConfig)?.content} content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "medium"} titleSize={(banner as BannerConfig)?.titleSize ?? "medium"}
showBreadcrumb={banner?.showBreadcrumb ?? false}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"} backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/> />
@ -48,7 +47,7 @@ export default function Sustainability() {
{section3Data.content} {section3Data.content}
</p> </p>
<div className={styles.socialResponsibilityCaseDataItems}> <div className={styles.socialResponsibilityCaseDataItems}>
{section3Data.items?.map((item, index) => ( {section3Data.items?.map((item: any, index: number) => (
<div <div
key={index} key={index}
className={styles.socialResponsibilityCaseDataItem} className={styles.socialResponsibilityCaseDataItem}
@ -68,7 +67,7 @@ export default function Sustainability() {
maskBackground="#F7FBFF" maskBackground="#F7FBFF"
> >
<div className={styles.socialResponsibilityReportData}> <div className={styles.socialResponsibilityReportData}>
{section4Data.items?.slice(0, sliceIndex).map((item, index) => ( {section4Data.items?.slice(0, sliceIndex).map((item: any, index: number) => (
<div <div
key={index} key={index}
className={styles.socialResponsibilityReportItem} className={styles.socialResponsibilityReportItem}

View File

@ -9,7 +9,7 @@ const isDev = process.env.DEV
console.log('isDev', isDev) console.log('isDev', isDev)
module.exports = { module.exports = {
'/companyHome': { '/companyHome': {
target: isDev ? 'http://10.3.0.24:9211/' : 'https://companyapi.batiao8.com/', target: isDev ? 'http://10.3.0.7:9999/' : 'https://companyapi.batiao8.com/',
// target: "http://10.3.0.24:9211/", // target: "http://10.3.0.24:9211/",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/companyHome/, '') rewrite: (path) => path.replace(/^\/companyHome/, '')

13
src/store/app.ts Normal file
View File

@ -0,0 +1,13 @@
import { create } from "zustand";
interface AppStoreState {
screenOpened: boolean;
setScreenOpened: (screenOpened: boolean) => void;
}
export const useAppStore = create<AppStoreState>()(
(set) => ({
screenOpened: false,
setScreenOpened: (screenOpened) => set({ screenOpened }),
}),
);

View File

@ -1,10 +1,8 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import type mockData from "@/api/mockData";
import type { LocaleKey, SupportLocale } from "@/type"; import type { LocaleKey, SupportLocale } from "@/type";
export type AppConfig = typeof mockData extends { "zh-CN": infer Z } ? Z : never; export type AppConfig = any;
export type I18nData = { "zh-CN": AppConfig; "en-US": AppConfig }; export type I18nData = { "zh-CN": AppConfig; "en-US": AppConfig };
/** 根据 navigator.language 映射到支持的 LocaleKey */ /** 根据 navigator.language 映射到支持的 LocaleKey */
@ -71,6 +69,7 @@ export const useStore = create<StoreState>()(
i18nData: s.i18nData, i18nData: s.i18nData,
token: s.token, token: s.token,
appConfig: s.i18nData?.[s.locale] ?? s.appConfig, appConfig: s.i18nData?.[s.locale] ?? s.appConfig,
supportLocales: s.supportLocales,
}), }),
} }
) )

View File

@ -0,0 +1,253 @@
export interface RawPageItem {
id: number;
name: string;
page: string;
path: string;
pid: number;
tags: string;
weight: number;
content: string;
create_time: string;
}
interface NavChild {
path: string;
label: string;
}
interface NavItem {
path: string;
label: string;
children?: NavChild[];
}
function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
function setNested(obj: Record<string, any>, keyPath: string, value: any) {
const keys = keyPath.split(".");
let cur = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!cur[keys[i]] || typeof cur[keys[i]] !== "object") cur[keys[i]] = {};
cur = cur[keys[i]];
}
const lastKey = keys[keys.length - 1];
if (cur[lastKey] && typeof cur[lastKey] === "object" && typeof value === "object" && value !== null) {
Object.assign(cur[lastKey], value);
} else {
cur[lastKey] = value;
}
}
const PATH_OVERRIDES: Record<string, string> = {
"/property-service": "business.propertyService",
};
function resolveKeyPath(
item: RawPageItem,
itemMap: Map<number, RawPageItem>,
): string | null {
const { page, pid, tags, name } = item;
if (page === "/") {
if (tags === "menu") return "home";
if (name === "全局配置") return "__global__";
return null;
}
if (PATH_OVERRIDES[page]) return PATH_OVERRIDES[page];
if (page.includes("{")) {
const base = page.replace(/\/\{[^}]+\}$/, "");
const segments = base.split("/").filter(Boolean);
return segments.map(kebabToCamel).join(".") + "Detail";
}
const segments = page.split("/").filter(Boolean);
if (segments.length >= 2) {
return segments.map(kebabToCamel).join(".");
}
if (segments.length === 1) {
if (pid > 0) {
const parent = itemMap.get(pid);
if (parent && parent.page !== "/" && parent.page) {
const parentSegments = parent.page.split("/").filter(Boolean);
const parentKey = parentSegments.map(kebabToCamel).join(".");
return parentKey + "." + kebabToCamel(segments[0]);
}
}
if (tags === "menu") {
return kebabToCamel(segments[0]);
}
return "others." + kebabToCamel(segments[0]);
}
return null;
}
function buildNavItems(
menuItems: RawPageItem[],
locale: "ZH" | "EN",
parsedContentMap: Map<number, Record<string, any>>,
): NavItem[] {
const topLevel = menuItems
.filter((it) => it.pid === 0)
.sort((a, b) => b.weight - a.weight);
return topLevel.map((item) => {
const children = menuItems
.filter((c) => c.pid === item.id)
.sort((a, b) => b.weight - a.weight);
const label =
locale === "EN"
? (parsedContentMap.get(item.id)?.EN?.banner?.title ?? item.name)
: item.name;
const navItem: NavItem = { label, path: item.page };
if (children.length > 0) {
navItem.children = children.map((c) => {
const childLabel =
locale === "EN"
? (parsedContentMap.get(c.id)?.EN?.banner?.title ?? c.name)
: c.name;
return { path: c.page, label: childLabel };
});
}
return navItem;
});
}
const DEFAULT_FOOTER_ZH = {
lowerLinks: [
{ path: "/contact-us", label: "联系我们" },
{ path: "/site-map", label: "网站地图" },
{ path: "/terms-of-use", label: "使用条款" },
{ path: "/privacy-policy", label: "隐私保护" },
{ path: "/audit-report", label: "审计举报" },
],
socialIcons: [
{ src: "/images/icon-weixin.png", alt: "weixin" },
{ src: "/images/icon-weibo.png", alt: "weibo" },
{ src: "/images/logo.png", alt: "logo" },
],
copyright: "版权声明©2001-{year} | 中国银泰投资有限公司",
icpNumber: "京ICP备05026114号-1",
};
const DEFAULT_FOOTER_EN = {
...DEFAULT_FOOTER_ZH,
lowerLinks: [
{ path: "/contact-us", label: "Contact" },
{ path: "/site-map", label: "Site Map" },
{ path: "/terms-of-use", label: "Terms of Use" },
{ path: "/privacy-policy", label: "Privacy Policy" },
{ path: "/audit-report", label: "Audit Report" },
],
copyright: "Copyright ©2001-{year} | China Yintai Investment Co., Ltd.",
};
/**
* Restructure global config to match the expected layout:
* API gives { company: { favicon, shortName, logo } }
* Pages need { company: { config: { favicon, shortName } }, logo: "..." }
*/
function mergeGlobalConfig(target: Record<string, any>, content: Record<string, any>) {
const { company, ...rest } = content;
Object.assign(target, rest);
if (company) {
const { logo, ...configFields } = company;
target.company = { config: configFields };
if (logo) target.logo = logo;
}
}
export function parsePageConfig(items: RawPageItem[]): {
"zh-CN": Record<string, any>;
"en-US": Record<string, any>;
} {
const filtered = items.filter((item) => item.tags !== "backend");
const itemMap = new Map<number, RawPageItem>();
for (const item of filtered) {
itemMap.set(item.id, item);
}
const parsedContentMap = new Map<number, Record<string, any>>();
for (const item of filtered) {
if (item.content && item.content.trim()) {
try {
parsedContentMap.set(item.id, JSON.parse(item.content));
} catch {
// skip invalid json
}
}
}
const menuItems = filtered.filter((it) => it.tags === "menu");
const zhNavItems = buildNavItems(menuItems, "ZH", parsedContentMap);
const enNavItems = buildNavItems(menuItems, "EN", parsedContentMap);
const zhConfig: Record<string, any> = {};
const enConfig: Record<string, any> = {};
for (const item of filtered) {
const keyPath = resolveKeyPath(item, itemMap);
if (!keyPath) continue;
const parsed = parsedContentMap.get(item.id) ?? {};
const zhContent = parsed.ZH ?? {};
const enContent = parsed.EN ?? {};
// if (keyPath === "__global__") {
// mergeGlobalConfig(zhConfig, zhContent);
// if (Object.keys(enContent).length > 0) {
// mergeGlobalConfig(enConfig, enContent);
// }
// continue;
// }
if (Object.keys(zhContent).length > 0) {
setNested(zhConfig, keyPath, zhContent);
}
if (Object.keys(enContent).length > 0) {
setNested(enConfig, keyPath, enContent);
}
}
zhConfig.header = { navItems: zhNavItems };
zhConfig.footer = { ...DEFAULT_FOOTER_ZH };
enConfig.header = { navItems: enNavItems };
enConfig.footer = { ...DEFAULT_FOOTER_EN };
deepFallback(enConfig, zhConfig);
return { "zh-CN": zhConfig, "en-US": enConfig };
}
/**
* For any key present in source but missing in target, copy it over.
* This ensures EN falls back to ZH when EN content is empty.
*/
function deepFallback(target: Record<string, any>, source: Record<string, any>) {
for (const key of Object.keys(source)) {
if (!(key in target)) {
target[key] = source[key];
} else if (
typeof target[key] === "object" &&
target[key] !== null &&
!Array.isArray(target[key]) &&
typeof source[key] === "object" &&
source[key] !== null &&
!Array.isArray(source[key])
) {
deepFallback(target[key], source[key]);
}
}
}