save
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 977 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.5 MiB |
|
|
@ -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 |
13
src/App.css
|
|
@ -124,4 +124,17 @@ body {
|
|||
color: #222222;
|
||||
line-height: 34px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* hover缩放 */
|
||||
.hover-scale {
|
||||
.hover-scale-bg {
|
||||
transition: transform 0.8s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-scale-bg {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import routes from "@/Routes";
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import appApi from "@/api/app";
|
||||
import { useStore } from "@/store";
|
||||
import { parsePageConfig } from "@/utils/parsePageConfig";
|
||||
import type { I18nData, SupportLocale } from "@/type";
|
||||
|
||||
function AppRoutes() {
|
||||
|
|
@ -20,7 +21,6 @@ function App() {
|
|||
const company = data?.company as { config?: { favicon?: string; shortName?: string } } | undefined;
|
||||
const { favicon, shortName } = company?.config || {};
|
||||
if (favicon) {
|
||||
// 替换网页图标
|
||||
const favor: any = document.querySelector("link[rel*='icon']") || document.createElement('link');
|
||||
favor.type = 'image/x-icon';
|
||||
favor.rel = 'shortcut icon';
|
||||
|
|
@ -34,10 +34,10 @@ function App() {
|
|||
const getAppConfig = useCallback(async () => {
|
||||
try {
|
||||
const res = await appApi.getAppConfig();
|
||||
const data = res.data as I18nData;
|
||||
const data = parsePageConfig(res.data.items) as I18nData;
|
||||
useStore.getState().setAppConfig(data);
|
||||
const locale = useStore.getState().locale;
|
||||
const config = data[locale] ?? data["zh-CN"];
|
||||
const currentLocale = useStore.getState().locale;
|
||||
const config = data[currentLocale] ?? data["zh-CN"];
|
||||
const supportLocales: SupportLocale[] = [
|
||||
{ key: "zh-CN", label: "中文" },
|
||||
{ key: "en-US", label: "English" },
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ const routes: RouteObject[] = [
|
|||
{ path: "join/culture", element: <JoinCulture /> },
|
||||
{ path: "join/campus", element: <JoinCampus /> },
|
||||
{ path: "join/campus/detail/:id", element: <JoinCampusDetail /> },
|
||||
// 其它
|
||||
// 物业服务
|
||||
{ path: "/property-service", element: <PropertyService /> },
|
||||
// 搜索
|
||||
{ path: "/search", element: <Search /> },
|
||||
// 其它
|
||||
// 使用条款 隐私保护 审计举报 网站地图
|
||||
{ path: "/terms-of-use", element: <TermsOfUse /> },
|
||||
{ path: "/privacy-policy", element: <PrivacyPolicy /> },
|
||||
|
|
|
|||
|
|
@ -1,23 +1,74 @@
|
|||
import requests from "@/utils/request";
|
||||
import mockData from "./mockData"
|
||||
|
||||
type Params = {page:number, size:number, sort?:string}
|
||||
const app = {
|
||||
getAppConfig() {
|
||||
// return requests({
|
||||
// url: "/api/config",
|
||||
// method: "get",
|
||||
// });
|
||||
return Promise.resolve({data: mockData});
|
||||
},
|
||||
getJobList(params: any) {
|
||||
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",
|
||||
params
|
||||
});
|
||||
},
|
||||
getJobDetail(id: number | string) {
|
||||
return requests({
|
||||
url: `/api/job/${id}`,
|
||||
url: `/yt/api/job/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
|
|
|||
1067
src/api/mockData.ts
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 },
|
||||
},
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
@ -6,7 +6,8 @@ import "swiper/css/effect-fade";
|
|||
import styles from "./Banner.module.css";
|
||||
import { useMemo } from "react";
|
||||
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%)";
|
||||
|
||||
|
|
@ -18,7 +19,6 @@ type Props = {
|
|||
desc?: string;
|
||||
content?: string;
|
||||
largedesc?: string;
|
||||
showBreadcrumb?: boolean;
|
||||
titleSize?: "large" | "medium" | string;
|
||||
backgroundImage: string | string[];
|
||||
};
|
||||
|
|
@ -29,7 +29,6 @@ export default function Banner({
|
|||
desc,
|
||||
content,
|
||||
largedesc,
|
||||
showBreadcrumb = true,
|
||||
titleSize = "large",
|
||||
backgroundImage,
|
||||
}: Props) {
|
||||
|
|
@ -42,6 +41,8 @@ export default function Banner({
|
|||
const isCarousel = images.length > 1;
|
||||
const descText = desc ?? content;
|
||||
|
||||
|
||||
const notShowBreadcrumbPaths = ["/"];
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
const segments = location.pathname.split("/").filter((s) => s !== "");
|
||||
if (segments.length === 0) {
|
||||
|
|
@ -52,11 +53,11 @@ export default function Banner({
|
|||
paths.push((paths[i - 1] ?? "") + "/" + segments[i]);
|
||||
}
|
||||
const getLabelByPath = (path: string): string => {
|
||||
if (path === "/") return navItems.find((n) => n.index)?.label ?? "首页";
|
||||
const top = navItems.find((n) => n.path === path);
|
||||
if (path === "/") return navItems.find((n: NavItem) => n.path === "/")?.label ?? "首页";
|
||||
const top = navItems.find((n: NavItem) => n.path === path);
|
||||
if (top) return top.label;
|
||||
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;
|
||||
}
|
||||
const last = path.split("/").pop() ?? path;
|
||||
|
|
@ -67,7 +68,7 @@ export default function Banner({
|
|||
to: path,
|
||||
}));
|
||||
items.unshift({
|
||||
label: navItems.find((n) => n.index)?.label ?? "首页",
|
||||
label: navItems.find((n: NavItem) => n.path === "/")?.label ?? "首页",
|
||||
to: "/",
|
||||
});
|
||||
return items;
|
||||
|
|
@ -75,19 +76,29 @@ export default function Banner({
|
|||
|
||||
const heroContent = (
|
||||
<div className={styles.heroContent} style={{ gap: "30px" }}>
|
||||
<h1 className={`${styles.heroTitle} ${titleSize === "medium" ? styles.heroTitleMedium : ""}`}>{title}</h1>
|
||||
{subtitle && <h2 className={styles.heroSubtitle}>{subtitle}</h2>}
|
||||
{descText && <p className={styles.heroDesc}>{descText}</p>}
|
||||
{largedesc && <p className={styles.heroLargeDesc}>{largedesc}</p>}
|
||||
<div className={styles.breadcrumb}>
|
||||
{showBreadcrumb &&
|
||||
(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 preset="slideUp">
|
||||
<h1 className={`${styles.heroTitle} ${titleSize === "medium" ? styles.heroTitleMedium : ""}`}>{title}</h1>
|
||||
</ScrollReveal>
|
||||
{subtitle && <ScrollReveal preset="slideUp" delay={0.2}>
|
||||
<h2 className={styles.heroSubtitle}>{subtitle}</h2>
|
||||
</ScrollReveal>}
|
||||
{descText && <ScrollReveal preset="slideUp" delay={0.2}>
|
||||
<p className={styles.heroDesc}>{descText}</p>
|
||||
</ScrollReveal>}
|
||||
{largedesc && <ScrollReveal preset="slideUp" delay={0.2}>
|
||||
<p className={styles.heroLargeDesc}>{largedesc}</p>
|
||||
</ScrollReveal>}
|
||||
<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>
|
||||
);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ type Data = {
|
|||
title: string;
|
||||
content: string;
|
||||
backgroundImage: string;
|
||||
link: string;
|
||||
path: string;
|
||||
moreText: string;
|
||||
}
|
||||
|
||||
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.cardContent}>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import styles from "./index.module.css";
|
|||
type Data = {
|
||||
title: string;
|
||||
createTime: string;
|
||||
readTimes: string;
|
||||
readTimes: string | number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/* 定义 */
|
||||
:root {
|
||||
--duration: 0.8s;
|
||||
}
|
||||
|
||||
.columnXGrids {
|
||||
/* 3列 */
|
||||
display: grid;
|
||||
|
|
@ -7,12 +12,23 @@
|
|||
.columnXGridsItem {
|
||||
color: #fff;
|
||||
aspect-ratio: 446/430;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -28,7 +44,7 @@
|
|||
height: 100%;
|
||||
z-index: 2;
|
||||
padding: 60px 40px;
|
||||
transition: background 0.3s ease;
|
||||
transition: background var(--duration) ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(20,53,92,0.7);
|
||||
|
|
@ -51,7 +67,7 @@
|
|||
color: #FFFFFF;
|
||||
line-height: 34px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
transition: opacity var(--duration) ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ export default function ColumnXGrids({ items }: Props) {
|
|||
return (
|
||||
<div className={styles.columnXGrids}>
|
||||
{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.columnXGridsItemInner}>
|
||||
<div className={styles.columnXGridsItemTitle}>{item.title}</div>
|
||||
|
|
|
|||
|
|
@ -44,4 +44,26 @@
|
|||
font-size: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ type Data = {
|
|||
recruitNumber: string;
|
||||
jobLocation: string;
|
||||
content: string;
|
||||
requirement: string;
|
||||
contact: string;
|
||||
}
|
||||
|
||||
export default function JobPage({ data }: { data: Data }) {
|
||||
|
|
@ -31,7 +33,16 @@ export default function JobPage({ data }: { data: Data }) {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
.rowAccordionBgLayer {
|
||||
position: absolute;
|
||||
|
|
@ -19,7 +20,8 @@
|
|||
background-size: cover;
|
||||
background-position: center;
|
||||
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 .contentRow {
|
||||
|
|
|
|||
|
|
@ -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 { 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%)";
|
||||
|
||||
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 = {
|
||||
title?: string;
|
||||
items: {
|
||||
|
|
@ -28,8 +55,18 @@ type Props = {
|
|||
|
||||
export default function RowAccordion({ data, placement='bottom' }: Props) {
|
||||
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 (
|
||||
<div className={styles.rowAccordion}>
|
||||
<div ref={containerRef} className={styles.rowAccordion}>
|
||||
<div className={styles.rowAccordionBgContainer}>
|
||||
{data.items.map((item, index) => (
|
||||
<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%' }}>
|
||||
{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' }}
|
||||
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.contentItemTitle}>{item.title}</div>
|
||||
|
|
@ -75,7 +123,7 @@ export default function RowAccordion({ data, placement='bottom' }: Props) {
|
|||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import styles from "./Footer.module.css";
|
||||
import { useStore } from "@/store";
|
||||
import type { NavItem, NavChild } from "@/type";
|
||||
|
||||
export default function Footer() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const footer = appConfig?.footer;
|
||||
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 socialIcons = footer?.socialIcons ?? [];
|
||||
const copyright = footer?.copyright ?? "版权声明©2001-{year} | 中国银泰投资有限公司";
|
||||
|
|
@ -18,10 +19,10 @@ export default function Footer() {
|
|||
<div className={styles.footerUpper}>
|
||||
<div>
|
||||
<div className={styles.footerGrid}>
|
||||
{footerColumns.map((col) => (
|
||||
{footerColumns.map((col: NavItem) => (
|
||||
<div key={col.path} className={styles.footerColumn}>
|
||||
<div className={styles.footerTitle}>{col.label}</div>
|
||||
{col.children?.map((link) => (
|
||||
{col.children?.map((link: NavChild) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
|
|
@ -33,7 +34,7 @@ export default function Footer() {
|
|||
</div>
|
||||
))}
|
||||
<div className={styles.footerRight}>
|
||||
{socialIcons.map((icon) => (
|
||||
{socialIcons.map((icon: { src: string; alt: string }) => (
|
||||
<img
|
||||
key={icon.alt}
|
||||
src={icon.src}
|
||||
|
|
|
|||
|
|
@ -17,17 +17,19 @@
|
|||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.hoverMenu.header {
|
||||
.whiteMode.header {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hoverMenu .navLink {
|
||||
.whiteMode .navLink {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.hoverMenu {
|
||||
.whiteMode {
|
||||
.searchBtn {
|
||||
color: #222222;
|
||||
}
|
||||
|
|
@ -77,6 +79,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.875rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
|
@ -110,11 +113,19 @@
|
|||
|
||||
.actions {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-left: 1.875rem;
|
||||
}
|
||||
|
||||
.crossYline {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: #FFFFFF;
|
||||
transition: height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.langTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -142,11 +153,17 @@
|
|||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 23.75rem;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 1.25rem;
|
||||
height: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
/* box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); */
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
transition: height 0.9s ease-in-out;
|
||||
|
||||
&.visible {
|
||||
height: 23.75rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropPanelContent {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Dropdown } from "antd";
|
||||
import styles from "./Header.module.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
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[] }[] = [];
|
||||
|
||||
|
|
@ -14,9 +15,9 @@ export default function Header() {
|
|||
const setLocale = useStore((s) => s.setLocale);
|
||||
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 logo = appConfig?.logo ?? "/images/logo.png";
|
||||
const logo = appConfig?.company?.logo ?? "/images/logo.png";
|
||||
|
||||
const [activeNav, setActiveNav] = useState("");
|
||||
const [showDropPanel, setShowDropPanel] = useState(false);
|
||||
|
|
@ -31,38 +32,33 @@ export default function Header() {
|
|||
}
|
||||
|
||||
const activePanelItem = useMemo(() => {
|
||||
return navItems.find((item) => item.path === activeNav)?.children || [];
|
||||
return navItems.find((item: NavItem) => item.path === activeNav)?.children || [];
|
||||
}, [activeNav]);
|
||||
|
||||
const [showWhiteMode, setShowWhiteMode] = useState(false);
|
||||
const isDetailPage = location.pathname.includes("/detail/");
|
||||
const isDetailPageRef = useRef(isDetailPage);
|
||||
isDetailPageRef.current = isDetailPage;
|
||||
|
||||
useEffect(() => {
|
||||
const path = location.pathname;
|
||||
if (path.includes("/detail/")) {
|
||||
if (isDetailPage) {
|
||||
setShowWhiteMode(true);
|
||||
} else {
|
||||
setShowWhiteMode(false);
|
||||
return;
|
||||
}
|
||||
}, [location.pathname]);
|
||||
setShowWhiteMode(window.scrollY > 100);
|
||||
}, [isDetailPage]);
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
if (scrollTop > 100) {
|
||||
setShowWhiteMode(true);
|
||||
} else {
|
||||
setShowWhiteMode(false);
|
||||
}
|
||||
}
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
if (isDetailPageRef.current) return;
|
||||
setShowWhiteMode(window.scrollY > 100);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className={`${styles.header} ${(showDropPanel || showWhiteMode) ? styles.hoverMenu : ""}`}
|
||||
<header className={`${styles.header} ${(showDropPanel || showWhiteMode) ? styles.whiteMode : ""}`}
|
||||
onMouseLeave={() => setShowDropPanel(false)}
|
||||
>
|
||||
<div className={`header-row ${styles.headerInner}`}>
|
||||
|
|
@ -72,7 +68,7 @@ export default function Header() {
|
|||
<div className={styles.headerRight}>
|
||||
<nav>
|
||||
<ul className={styles.nav}>
|
||||
{navItems.map((item) => (
|
||||
{navItems.map((item: NavItem) => (
|
||||
<li key={item.path} className={styles.navItem} >
|
||||
<div className={styles.navLink} onMouseEnter={(e) => handleNavEnter(e, item.path)}>
|
||||
{item.label}
|
||||
|
|
@ -81,6 +77,7 @@ export default function Header() {
|
|||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className={styles.crossYline}></div>
|
||||
<div className={styles.actions}>
|
||||
<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>
|
||||
|
|
@ -106,14 +103,31 @@ export default function Header() {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<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}>
|
||||
{items.map((item) => (
|
||||
<div key={item.path} className={styles.dropPanelItem}>
|
||||
|
|
|
|||
|
|
@ -23,14 +23,13 @@ export default function AboutFounder() {
|
|||
title={banner?.title ?? ""}
|
||||
subtitle={banner?.subtitle}
|
||||
desc={banner?.content}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
{section1Data && (
|
||||
<ParagraphSection data={section1Data}>
|
||||
<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}>
|
||||
<img src={item.backgroundImage} alt={item.title} />
|
||||
<div className={styles.imageMask} />
|
||||
|
|
@ -73,9 +72,9 @@ export default function AboutFounder() {
|
|||
<Section title={section3Data?.title} titleColor="#fff" background={section3Data?.backgroundImage}>
|
||||
<div className={styles.section3Content}>
|
||||
{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) =>
|
||||
(section3Data?.columns ?? []).map((colItems, colIndex) => (
|
||||
(section3Data?.columns ?? []).map((colItems: { title?: string }[], colIndex: number) => (
|
||||
<div key={`${rowIndex}-${colIndex}`} className={styles.section3Item}>
|
||||
{colItems[rowIndex] ? <li>{colItems[rowIndex].title}</li> : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
align-items: flex-start;
|
||||
min-height: 6rem;
|
||||
margin-bottom: 1rem;
|
||||
scroll-margin-top: 120px;
|
||||
}
|
||||
|
||||
.timelineItem .side {
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background: #14355C;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.dotWrapper.selected::before {
|
||||
|
|
@ -136,6 +138,7 @@
|
|||
border: 2px solid #14355C;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.dotLine {
|
||||
|
|
@ -146,8 +149,10 @@
|
|||
width: 0.375rem;
|
||||
min-height: 0;
|
||||
background: #14355C;
|
||||
transition: all 0.9s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.section {
|
||||
padding: 4rem 4rem;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import Banner from "@/components/Banner";
|
||||
import YearPicker from "@/components/YearPicker";
|
||||
import styles from "./History.module.css";
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react";
|
||||
import { useStore } from "@/store";
|
||||
|
||||
type TimelineItem = { year: number; content: string };
|
||||
import appApi from "@/api/app";
|
||||
type TimelineItem = { year: number; content: string, lang: string };
|
||||
|
||||
export default function AboutHistory() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const locale = useStore((s) => s.locale);
|
||||
|
||||
const history = appConfig?.about?.history;
|
||||
const banner = history?.banner;
|
||||
const section1Data = (history?.section1Data ?? []) as TimelineItem[];
|
||||
|
||||
const [year, setYear] = useState<number | null>(null);
|
||||
|
||||
const handleYearChange = (year: number | null) => {
|
||||
setYear(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;
|
||||
|
||||
return (
|
||||
|
|
@ -29,7 +47,6 @@ export default function AboutHistory() {
|
|||
<Banner
|
||||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -38,7 +55,7 @@ export default function AboutHistory() {
|
|||
<YearPicker placeholder="年份" value={year} onChange={handleYearChange} />
|
||||
</div>
|
||||
<HistoryTimeline
|
||||
data={section1Data}
|
||||
data={localProcessList}
|
||||
selectedYear={year}
|
||||
onYearChange={setYear}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
|
|||
import styles from "./About.module.css";
|
||||
import Banner from "@/components/Banner";
|
||||
import { useStore } from "@/store";
|
||||
import ScrollReveal from "@/components/ScrollReveal";
|
||||
|
||||
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ export default function About() {
|
|||
const overview = appConfig?.about?.overview;
|
||||
|
||||
const banner = overview?.banner;
|
||||
const section1Data = overview?.section1Data ?? [];
|
||||
const section1Data = overview?.section1Data?.items ?? [];
|
||||
|
||||
if (!overview) return null;
|
||||
|
||||
|
|
@ -19,11 +20,10 @@ export default function About() {
|
|||
<Banner
|
||||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
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
|
||||
key={index}
|
||||
className={`${styles.section} ${index % 2 === 1 ? styles.sectionRight : ""}`}
|
||||
|
|
@ -34,17 +34,19 @@ export default function About() {
|
|||
<div
|
||||
className={`${styles.content} ${index % 2 === 1 ? styles.contentRight : ""}`}
|
||||
>
|
||||
<div className={styles.textBox}>
|
||||
<h2 className={styles.title}>{item.title}</h2>
|
||||
<p className={styles.desc}>{item.content}</p>
|
||||
<div className={styles.links}>
|
||||
{item.links?.map((link) => (
|
||||
<span key={link.label}>
|
||||
<Link to={link.to}>{link.label}</Link>
|
||||
</span>
|
||||
))}
|
||||
<ScrollReveal preset="slideUp">
|
||||
<div className={styles.textBox}>
|
||||
<h2 className={styles.title}>{item.title}</h2>
|
||||
<p className={styles.desc}>{item.content}</p>
|
||||
<div className={styles.links}>
|
||||
{item.links?.map((link: { label: string; path: string }) => (
|
||||
<span key={link.label}>
|
||||
<Link to={link.path}>{link.label}</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export default function BaseGroup() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export default function BusinessCommercialGroup() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export default function BusinessCommercialGroupDetail() {
|
|||
title={banner?.title ?? ""}
|
||||
largedesc={(banner as BannerConfig)?.largeContent}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-commercial-group.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export default function InvestGroup() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export default function RealtyGroup() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export default function RuijingGroup() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -260,13 +260,13 @@
|
|||
|
||||
.newsFeatured {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.newsFeaturedImgWrap {
|
||||
width: 59.375rem;
|
||||
aspect-ratio: 950 / 560;
|
||||
background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 100%);
|
||||
background: red;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -294,6 +294,12 @@
|
|||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.newsItemDateWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.newsItem {
|
||||
box-sizing: border-box;
|
||||
padding: 1.5rem 1.875rem 1.25rem 1.875rem;
|
||||
|
|
@ -302,15 +308,36 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: #222222;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.4;
|
||||
transition: color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.newsItemTitle a {
|
||||
|
|
@ -327,6 +354,7 @@
|
|||
color: #666;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.6;
|
||||
height: 75px;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
|
|
@ -339,3 +367,7 @@
|
|||
font-size: 0.875rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import { useStore } from "@/store";
|
||||
import { Link } from "react-router-dom";
|
||||
import styles from "./Home.module.css";
|
||||
import Banner from "@/components/Banner";
|
||||
import Section from "@/components/layout/Section";
|
||||
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() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
|
|
@ -11,76 +20,168 @@ export default function Home() {
|
|||
|
||||
const banner = home?.banner;
|
||||
const section1Data = home?.section1Data;
|
||||
const section2Data = home?.section2Data ?? [];
|
||||
const newsData = home?.newsData ?? [];
|
||||
const section2Data = home?.section2Data?.items ?? [];
|
||||
|
||||
if (!home) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 开屏动画 */}
|
||||
<ScreenOpen />
|
||||
{/* Hero */}
|
||||
<Banner
|
||||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
{/* Commercial */}
|
||||
{section1Data && <RowAccordion data={section1Data} />}
|
||||
{section1Data &&
|
||||
<RowAccordion data={section1Data} />
|
||||
}
|
||||
|
||||
{/* 首页名片 */}
|
||||
<Section background="/images/bg-mask-card.png">
|
||||
<div className={styles.cardsInner}>
|
||||
{section2Data.map((item, i) => (
|
||||
<div key={i} className={`${styles.card} ${styles.cardSustainable}`}>
|
||||
<div
|
||||
className={styles.cardBg}
|
||||
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>
|
||||
</div>
|
||||
{section2Data.map((item: any, i: number) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "0px 0px -30% 0px" }}
|
||||
variants={cardVariants}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
delay: i * 0.15,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Section>
|
||||
|
||||
{/* 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>
|
||||
<News />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 Banner, { type BannerConfig } from "@/components/Banner";
|
||||
import Section from "@/components/layout/Section";
|
||||
|
|
@ -8,16 +8,20 @@ import { Empty, Select } from "antd";
|
|||
import Pagination from "@/components/Pagination";
|
||||
import { Link } from "react-router-dom";
|
||||
import { debounce } from "@/utils";
|
||||
import appApi from "@/api/app";
|
||||
|
||||
type JobItem = {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
labels: string[];
|
||||
lang: string
|
||||
}
|
||||
|
||||
export default function JoinCampus() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const supportLocales = useStore((s) => s.supportLocales);
|
||||
const locale = useStore((s) => s.locale);
|
||||
const data = appConfig?.join?.campus;
|
||||
const banner = data?.banner;
|
||||
|
||||
|
|
@ -25,37 +29,69 @@ export default function JoinCampus() {
|
|||
|
||||
// 职业类别 业务领域 所属板块
|
||||
const [jobType, setJobType] = useState('');
|
||||
const [jobTypeOptions, setJobTypeOptions] = useState([
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "职业类别1", value: "1" },
|
||||
{ label: "职业类别2", value: "2" },
|
||||
{ label: "职业类别3", value: "3" },
|
||||
]);
|
||||
const [jobTypeOptions, setJobTypeOptions] = useState([]);
|
||||
const [businessArea, setBusinessArea] = useState('');
|
||||
const [businessAreaOptions, setBusinessAreaOptions] = useState([]);
|
||||
const [businessPlate, setBusinessPlate] = useState('');
|
||||
const [businessPlateOptions, setBusinessPlateOptions] = useState([]);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [size] = useState(9);
|
||||
const [size] = useState(2 * supportLocales.length);
|
||||
const [total, setTotal] = useState(1000);
|
||||
|
||||
|
||||
const [jobList, setJobList] = useState<JobItem[]>([]);
|
||||
const localJobList = useMemo(() => {
|
||||
return jobList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
|
||||
}, [jobList, locale]);
|
||||
|
||||
const refreshData = useCallback(debounce(() => {
|
||||
console.log('refreshData2');
|
||||
setJobList([
|
||||
{ id: 1, title: '职位1', content: '工作职责:1、为集团的各类投资项目提供法律支持,包括:组织外部律师进行尽职调查、审阅及起草交易文件、必要时参与法律谈判、提示法律风险等;2、协助完善集团本部的规章制度和合规体系;3、审阅集团各部门提交法务部审阅的日常业务法务部审阅的日常业务法务部审阅的日常业务', labels: ['标签1', '标签2'] },
|
||||
{ id: 2, title: '职位2', content: '职位2内容', labels: ['标签1', '标签2'] },
|
||||
{ id: 3, title: '职位3', content: '职位3内容', labels: ['标签1', '标签2'] },
|
||||
])
|
||||
// 用 ref 保存最新参数,保证 debounce 使用稳定的函数引用
|
||||
const paramsRef = useRef({ searchValue, jobType, businessArea, businessPlate, page, size });
|
||||
paramsRef.current = { searchValue, jobType, businessArea, businessPlate, page, size };
|
||||
|
||||
const refreshData = useMemo(() => debounce(() => {
|
||||
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), []);
|
||||
|
||||
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(() => {
|
||||
refreshData();
|
||||
}, [searchValue, jobType, businessArea, businessPlate, page, size]);
|
||||
|
||||
useEffect(() => {
|
||||
getTypes();
|
||||
}, []);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setJobType('');
|
||||
|
|
@ -70,7 +106,6 @@ export default function JoinCampus() {
|
|||
title={banner?.title ?? "招贤纳士"}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -88,21 +123,21 @@ export default function JoinCampus() {
|
|||
<div className={styles.campusColRight}>
|
||||
<div className={styles.jobList}>
|
||||
{/* 没有数据时显示 */}
|
||||
{jobList.length === 0 && (
|
||||
{localJobList.length === 0 && (
|
||||
<div className={styles.noData}>
|
||||
<Empty description="暂无数据" />
|
||||
</div>
|
||||
)}
|
||||
{jobList.map(item => (
|
||||
<Link to={`/join/campus/detail/${item.id}`}>
|
||||
<div key={item.id} className={styles.jobItem}>
|
||||
{localJobList.map((item, index) => (
|
||||
<Link key={item.id} to={`/join/campus/detail/${item.id}`}>
|
||||
<div className={styles.jobItem}>
|
||||
<div className={styles.jobItemTitleRow}>
|
||||
<div className={styles.jobItemTitle}>{item.title}</div>
|
||||
<div className={styles.jobItemTitleRight}>查看详情 <RightOutlined /></div>
|
||||
</div>
|
||||
<div className={styles.jobItemLabels}>
|
||||
{item.labels.map(label => (
|
||||
<div key={label} className={styles.jobItemLabel}> • {label}</div>
|
||||
{item.labels.map((label, index) => (
|
||||
<div key={index} className={styles.jobItemLabel}> • {label}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.jobItemContent}>{item.content}</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,46 @@
|
|||
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() {
|
||||
const data = {
|
||||
title: "项目营销总监",
|
||||
jobType: "营销总监",
|
||||
businessArea: "营销",
|
||||
businessPlate: "营销",
|
||||
recruitNumber: "2",
|
||||
jobLocation: "杭州",
|
||||
content: "国以才立,政以才治,业以才兴,做好新形势下立法工作,保障立法质量,人才队伍是关键。自2020年起,在银泰公益基金会的支持下,浙江立法研究院、浙江大学立法研究院联合浙江大学光华法学院致力于助推立法学科建设,经过一年多时间的不懈努力,获批增设立法学二级学科,并面向全国招收立法学专业硕士、博士研究生,旨在积极响应新时代下国家对立法学理论研究和人才培养的需求,践行依法护航经济社会发展的理念,不断开展探索,助推立法工作更好地为中国特色社会主义法治建设服务。",
|
||||
}
|
||||
const locale = useStore((s) => s.locale);
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const [jobDetail, setJobDetail] = useState<any>(null);
|
||||
const localJobDetail = useMemo(() => {
|
||||
return jobDetail?.find((item: any) => item.lang.toLowerCase() === locale.split('-')[0]);
|
||||
}, [jobDetail, locale]);
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<JobPage data={data} />
|
||||
{
|
||||
localJobDetail &&
|
||||
<JobPage data={localJobDetail} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ export default function Culture() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ export default function Culture() {
|
|||
maskBackground="#D8D8D8"
|
||||
>
|
||||
<div className={styles.cultureItems}>
|
||||
{section1Data.items?.map((item, index) => (
|
||||
{section1Data.items?.map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.cultureItem}
|
||||
|
|
@ -54,7 +53,7 @@ export default function Culture() {
|
|||
maskBackground="rgba(216,216,216,0.6)"
|
||||
>
|
||||
<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
|
||||
className={styles.valuesItemIcon}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,48 @@
|
|||
import Article from "@/components/layout/Article";
|
||||
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() {
|
||||
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 (
|
||||
<div>
|
||||
{articleData && <Article data={articleData} />}
|
||||
{newsDetail && <Article data={newsDetail} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,12 @@ export default function Media() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
<Section background="/images/bg-overview.png" maskBackground="rgba(1,11,72,0.8)">
|
||||
<div className={styles.mediaItems}>
|
||||
{items.map((item, index) => (
|
||||
{items.map((item: { title: string; content?: string }, index: number) => (
|
||||
<div key={index} className={styles.mediaItem}>
|
||||
<div className={styles.mediaItemTitle}>{item.title}</div>
|
||||
<div className={styles.mediaItemContent}>{item.content}</div>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
|
||||
.newList {
|
||||
/* 3列 */
|
||||
min-height: 200px;
|
||||
margin-top: 100px;
|
||||
margin-bottom: 100px;
|
||||
display: grid;
|
||||
|
|
@ -53,12 +54,13 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
img, video {
|
||||
width: 100%;
|
||||
/* height: 300px; */
|
||||
aspect-ratio: 452 / 300;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
@ -70,6 +72,10 @@
|
|||
.newItemCreateTime {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.newItemContent {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import styles from "./NewsPublic.module.css";
|
||||
import Banner, { type BannerConfig } from "@/components/Banner";
|
||||
import Section from "@/components/layout/Section";
|
||||
|
|
@ -6,27 +6,69 @@ import { SearchOutlined } from "@ant-design/icons";
|
|||
import Pagination from "@/components/Pagination";
|
||||
import { Link } from "react-router-dom";
|
||||
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() {
|
||||
const appConfig = useStore((s) => s.appConfig);
|
||||
const locale = useStore((s) => s.locale);
|
||||
const supportLocales = useStore((s) => s.supportLocales);
|
||||
const data = appConfig?.news?.public;
|
||||
|
||||
const [newList] = useState([
|
||||
{ id: 1, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" },
|
||||
{ id: 2, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" },
|
||||
{ id: 3, title: "新闻标题", createTime: "2026-03-03", image: "/images/bg-overview.png" },
|
||||
]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size] = useState(9 * supportLocales.length);
|
||||
const [total, setTotal] = useState(0);
|
||||
const categoryIdRef = useRef<string | null>('');
|
||||
|
||||
const [newList, setNewList] = useState<NewsItem[]>([]);
|
||||
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const handleSearch = useCallback(() => {
|
||||
console.log("search", searchValue);
|
||||
}, [searchValue]);
|
||||
appApi.getNewsList({ page, size, sort: "create_time DESC", title: 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 [size] = useState(9);
|
||||
const [total, setTotal] = useState(1000);
|
||||
const localNewsList = useMemo(() => {
|
||||
return newList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
|
||||
}, [newList, locale]);
|
||||
|
||||
const getCategoryList = useCallback(async () => {
|
||||
const res = await appApi.getCategoryList('news');
|
||||
const category_id = res.data.items.find((item: any) => item.name.includes('集团发布'))?.id;
|
||||
categoryIdRef.current = category_id;
|
||||
return category_id;
|
||||
}, []);
|
||||
|
||||
const banner = data?.banner;
|
||||
|
||||
useEffect(() => {
|
||||
getCategoryList().then(() => {
|
||||
handleSearch();
|
||||
})
|
||||
}, [page, size]);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -35,7 +77,6 @@ export default function NewsPublic() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -56,13 +97,33 @@ export default function NewsPublic() {
|
|||
</div>
|
||||
|
||||
<div className={styles.newList}>
|
||||
{newList.map((item, index) => (
|
||||
{localNewsList.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
to={`/news/detail/${item.id}`}
|
||||
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.newItemTitle}>{item.title}</div>
|
||||
<div className={styles.newItemCreateTime}>{item.createTime}</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export default function AuditReport() {
|
|||
title={banner?.title ?? "审计举报"}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export default function PrivacyPolicy() {
|
|||
title={banner?.title ?? "隐私保护"}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ export default function Search() {
|
|||
title={banner?.title ?? "搜索结果"}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/search-banner.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export default function SiteMap() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ export default function SiteMap() {
|
|||
<div className={styles.siteMapTitle}>{title}</div>
|
||||
|
||||
<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.siteMapItemLabel}>{item.label}</div>
|
||||
<div className={styles.siteMapItemChildren}>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export default function TermsOfUse() {
|
|||
title={banner?.title ?? "使用条款"}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export default function PropertyService() {
|
|||
title={banner?.title ?? ""}
|
||||
desc={banner?.content}
|
||||
titleSize={banner?.titleSize as "medium" | "large" | undefined ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export default function Foundation() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -44,13 +43,13 @@ export default function Foundation() {
|
|||
maskBackground="rgba(255,255,255,0.3)"
|
||||
>
|
||||
<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
|
||||
key={index}
|
||||
className={styles.publicWelfareDataItem}
|
||||
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>
|
||||
|
|
@ -67,7 +66,7 @@ export default function Foundation() {
|
|||
>
|
||||
<div className={styles.informationPublicDataContent}>
|
||||
<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}>
|
||||
<li>
|
||||
<span>{item.fileName}</span>
|
||||
|
|
@ -87,7 +86,7 @@ export default function Foundation() {
|
|||
{section4Data && (
|
||||
<Section title={section4Data.title} background="" maskBackground="#F7FBFF">
|
||||
<div className={styles.partnerItems}>
|
||||
{section4Data.items?.map((item, index) => (
|
||||
{section4Data.items?.map((item: any, index: number) => (
|
||||
<div key={index} className={styles.partnerItem}>
|
||||
<img src={item.logo} alt="logo" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function Sustainability() {
|
|||
|
||||
const banner = data.banner;
|
||||
const section1Data = data.section1Data;
|
||||
const section2Data = data.section2Data ?? [];
|
||||
const section2Data = data.section2Data?.items ?? [];
|
||||
const section3Data = data.section3Data;
|
||||
const section4Data = data.section4Data;
|
||||
|
||||
|
|
@ -28,7 +28,6 @@ export default function Sustainability() {
|
|||
title={banner?.title ?? ""}
|
||||
content={(banner as BannerConfig)?.content}
|
||||
titleSize={(banner as BannerConfig)?.titleSize ?? "medium"}
|
||||
showBreadcrumb={banner?.showBreadcrumb ?? false}
|
||||
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
|
||||
/>
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ export default function Sustainability() {
|
|||
{section3Data.content}
|
||||
</p>
|
||||
<div className={styles.socialResponsibilityCaseDataItems}>
|
||||
{section3Data.items?.map((item, index) => (
|
||||
{section3Data.items?.map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.socialResponsibilityCaseDataItem}
|
||||
|
|
@ -68,7 +67,7 @@ export default function Sustainability() {
|
|||
maskBackground="#F7FBFF"
|
||||
>
|
||||
<div className={styles.socialResponsibilityReportData}>
|
||||
{section4Data.items?.slice(0, sliceIndex).map((item, index) => (
|
||||
{section4Data.items?.slice(0, sliceIndex).map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.socialResponsibilityReportItem}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const isDev = process.env.DEV
|
|||
console.log('isDev', isDev)
|
||||
module.exports = {
|
||||
'/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/",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/companyHome/, '')
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
import type mockData from "@/api/mockData";
|
||||
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 };
|
||||
|
||||
/** 根据 navigator.language 映射到支持的 LocaleKey */
|
||||
|
|
@ -71,6 +69,7 @@ export const useStore = create<StoreState>()(
|
|||
i18nData: s.i18nData,
|
||||
token: s.token,
|
||||
appConfig: s.i18nData?.[s.locale] ?? s.appConfig,
|
||||
supportLocales: s.supportLocales,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||