This commit is contained in:
zhangjianjun 2026-03-06 10:01:55 +08:00
parent 8502fdc6c3
commit aa8358c91e
12 changed files with 189 additions and 58 deletions

View File

@ -4,7 +4,8 @@ import "./App.css"
import routes from "@/Routes"; 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, type I18nData } from "@/store"; import { useStore } from "@/store";
import type { I18nData, SupportLocale } from "@/type";
function AppRoutes() { function AppRoutes() {
return useRoutes(routes); return useRoutes(routes);
@ -36,7 +37,12 @@ function App() {
const data = res.data as I18nData; const data = res.data as I18nData;
useStore.getState().setAppConfig(data); useStore.getState().setAppConfig(data);
const locale = useStore.getState().locale; const locale = useStore.getState().locale;
const config = data[locale] ?? data.zhCN; const config = data[locale] ?? data["zh-CN"];
const supportLocales: SupportLocale[] = [
{ key: "zh-CN", label: "中文" },
{ key: "en-US", label: "English" },
];
useStore.getState().setSupportLocales(supportLocales);
initState(config); initState(config);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -1,14 +1,12 @@
/** /**
* *
* mockData: { * mockData: {
* zhCN: {...}, * "zh-CN": {...},
* en: {...} * "en-US": {...}
* } * }
*/ */
export type NavChild = { path: string; label: string }; import type { NavChild, NavItem } from "@/type";
export type NavItem = { path: string; label: string; children?: NavChild[]; index?: boolean };
export type LocaleKey = "zhCN" | "en";
const zhCN = { const zhCN = {
companyName: "银泰", companyName: "银泰",
@ -72,10 +70,6 @@ const zhCN = {
], ],
}, },
] as NavItem[], ] as NavItem[],
langMenuItems: [
{ key: "zhCN", label: "中文" },
{ key: "en", label: "English" },
],
}, },
footer: { footer: {
@ -309,7 +303,44 @@ const zhCN = {
backgroundImage: '', backgroundImage: '',
sideImage: "/images/bg-invest-group.png", sideImage: "/images/bg-invest-group.png",
content: "银泰集团在社会责任方面有着丰富的经验和深厚的实力,致力于打造高品质的商业空间,引领现代消费体验。", content: "银泰集团在社会责任方面有着丰富的经验和深厚的实力,致力于打造高品质的商业空间,引领现代消费体验。",
} },
section3Data: {
title: "社会职务",
backgroundImage: '/images/bg-overview.png',
columns: [
[ // 第 1 列
{ title: '第十一、十三届全国政协委员' },
{ title: '第十三届全国政协提案委员会委员' },
{ title: '第十四、十五、十六届中国致公党中央常委' },
{ title: '第一届浙商总会执行会长' },
],
[ // 第 2 列
{ title: '第一届甬商总会会长' },
{ title: '北京浙江企业商会终身名誉会长' },
{ title: '西安市人民政府经济顾问' },
{ title: '中国企业家俱乐部理事' },
],
[ // 第 3 列
{ title: '桃花源生态保护基金会执行主席' },
{ title: '银泰公益基金会创始人兼荣誉理事长' },
{ title: '中国宋庆龄基金会第六届理事会理事' },
{ title: '爱佑慈善基金会发起理事' },
{ title: '致福慈善基金会副理事长' },
],
]
},
section4Data: {
title: "荣誉奖项",
backgroundImage: '',
items: [
{ year: '2020年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
{ year: '2019年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
{ year: '2018年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
{ year: '2017年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
{ year: '2016年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
{ year: '2015年', children: ["2015年度“影响·2015中国公益100人”", "2015年度“中国社会十大推动者”"]},
],
},
}, },
}, },
@ -975,10 +1006,6 @@ const en: typeof zhCN = {
], ],
}, },
] as NavItem[], ] as NavItem[],
langMenuItems: [
{ key: "zhCN", label: "中文" },
{ key: "en", label: "English" },
],
}, },
footer: { footer: {
...zhCN.footer, ...zhCN.footer,
@ -1002,5 +1029,5 @@ const en: typeof zhCN = {
}; };
export type LocaleConfig = typeof zhCN; export type LocaleConfig = typeof zhCN;
const mockData = { zhCN, en }; const mockData = { "zh-CN": zhCN, "en-US": en };
export default mockData; export default mockData;

View File

@ -6,19 +6,11 @@ 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";
const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)"; const FALLBACK_GRADIENT = "linear-gradient(135deg, #1a2a4a 0%, #2d4a7c 100%)";
export type { BannerConfig } from "@/type";
export type BannerConfig = {
title?: string;
content?: string;
subtitle?: string;
largeContent?: string;
titleSize?: "large" | "medium" | string;
showBreadcrumb?: boolean;
backgroundImage?: string | string[];
};
type Props = { type Props = {
title: string; title: string;

View File

@ -86,6 +86,7 @@
margin-bottom: 6px; margin-bottom: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
white-space: nowrap;
} }
.contentItemSubtitle { .contentItemSubtitle {
font-size: 20px; font-size: 20px;
@ -93,6 +94,7 @@
} }
.contentItemContentWrapper { .contentItemContentWrapper {
box-sizing: border-box;
height: 0; height: 0;
opacity: 0; opacity: 0;
transition: all var(--duration) ease-in-out; transition: all var(--duration) ease-in-out;
@ -100,11 +102,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
padding: 60px 0; /* padding: 60px 0; */
margin-top: 20px;
} }
.contentItemContent { .contentItemContent {
font-size: 16px; font-size: 16px;
margin-top: 40px; /* margin-top: 40px; */
} }
.contentItemLinks { .contentItemLinks {

View File

@ -73,7 +73,7 @@ function FooterLower({
<div className={styles.footerLowerLinks}> <div className={styles.footerLowerLinks}>
{lowerLinks.map((link, index) => ( {lowerLinks.map((link, index) => (
< > < >
<Link to={link.path} className={styles.footerLowerLink}> <Link key={index} to={link.path} className={styles.footerLowerLink}>
{link.label} {link.label}
</Link> </Link>
{index !== lowerLinks.length - 1 && <span>·</span>} {index !== lowerLinks.length - 1 && <span>·</span>}

View File

@ -153,6 +153,7 @@
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
transform: translateX(-50%);
} }
.dropPanelLink { .dropPanelLink {

View File

@ -3,29 +3,29 @@ 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, useState } from "react";
import { useStore } from "@/store"; import { useStore } from "@/store";
import type { NavChild } from "@/api/mockData"; import type { NavChild, LocaleKey, SupportLocale } from "@/type";
import type { LocaleKey } from "@/store";
const DEFAULT_NAV_ITEMS: { path: string; label: string; children?: NavChild[] }[] = []; const DEFAULT_NAV_ITEMS: { path: string; label: string; children?: NavChild[] }[] = [];
const DEFAULT_LANG_ITEMS = [{ key: "zh", label: "中文" }, { key: "en", label: "English" }];
export default function Header() { export default function Header() {
const location = useLocation(); const location = useLocation();
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
const locale = useStore((s) => s.locale); const locale = useStore((s) => s.locale);
const setLocale = useStore((s) => s.setLocale); 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) => !item.index) ?? DEFAULT_NAV_ITEMS;
const langMenuItems = appConfig?.header?.langMenuItems ?? DEFAULT_LANG_ITEMS; const langMenuItems: SupportLocale[] = supportLocales || [];
const logo = appConfig?.logo ?? "/images/logo.png"; const logo = appConfig?.logo ?? "/images/logo.png";
const [activeNav, setActiveNav] = useState(""); const [activeNav, setActiveNav] = useState("");
const [showDropPanel, setShowDropPanel] = useState(false); const [showDropPanel, setShowDropPanel] = useState(false);
const [hoverElLeft, setHoverElLeft] = useState(0); const [hoverElLeft, setHoverElLeft] = useState(0);
const handleNavEnter = (e: any, path: string) => { const handleNavEnter = (e: any, path: string) => {
// left + 元素宽度的一半
const left = e.target.offsetLeft; const left = e.target.offsetLeft;
setHoverElLeft(left); // 计算元素宽度
const width = e.target.offsetWidth;
setHoverElLeft(left + width / 2);
setActiveNav(path); setActiveNav(path);
setShowDropPanel(true); setShowDropPanel(true);
} }
@ -87,9 +87,9 @@ export default function Header() {
</Link> </Link>
<Dropdown <Dropdown
menu={{ menu={{
items: langMenuItems.map((item) => ({ items: langMenuItems.map((item: SupportLocale) => ({
...item, ...item,
onClick: () => setLocale(item.key as LocaleKey), onClick: () => setLocale(item.key),
})), })),
}} }}
placement="bottomRight" placement="bottomRight"
@ -97,7 +97,7 @@ export default function Header() {
> >
<button className={styles.langTrigger} type="button"> <button className={styles.langTrigger} type="button">
<span style={{ fontSize: "18px" }}> <span style={{ fontSize: "18px" }}>
{langMenuItems.find((i) => i.key === locale)?.label ?? "中文"} {langMenuItems.find((i: SupportLocale) => i.key === locale)?.label ?? "中文"}
</span> </span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g transform="matrix(0,1,-1,0,24,-24)"><path d="M46.720364062499996,30.4020875C46.720364062499996,30.4020875,45.6597041625,31.462787499999997,45.6597041625,31.462787499999997C45.6597041625,31.462787499999997,39.8815300725,25.6845775,39.8815300725,25.6845775C39.4910054655,25.2940474,39.4910053615,24.6608877,39.8815300725,24.2703576C39.8815300725,24.2703576,45.6597041625,18.4921875,45.6597041625,18.4921875C45.6597041625,18.4921875,46.720364062499996,19.5528475,46.720364062499996,19.5528475C46.720364062499996,19.5528475,41.2957440625,24.9774675,41.2957440625,24.9774675C41.2957440625,24.9774675,46.720364062499996,30.4020875,46.720364062499996,30.4020875C46.720364062499996,30.4020875,46.720364062499996,30.4020875,46.720364062499996,30.4020875Z" fillRule="evenodd" fill="#FFFFFF" fillOpacity="1" transform="matrix(-1,0,0,-1,79.173828125,36.984375)" /></g></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g transform="matrix(0,1,-1,0,24,-24)"><path d="M46.720364062499996,30.4020875C46.720364062499996,30.4020875,45.6597041625,31.462787499999997,45.6597041625,31.462787499999997C45.6597041625,31.462787499999997,39.8815300725,25.6845775,39.8815300725,25.6845775C39.4910054655,25.2940474,39.4910053615,24.6608877,39.8815300725,24.2703576C39.8815300725,24.2703576,45.6597041625,18.4921875,45.6597041625,18.4921875C45.6597041625,18.4921875,46.720364062499996,19.5528475,46.720364062499996,19.5528475C46.720364062499996,19.5528475,41.2957440625,24.9774675,41.2957440625,24.9774675C41.2957440625,24.9774675,46.720364062499996,30.4020875,46.720364062499996,30.4020875C46.720364062499996,30.4020875,46.720364062499996,30.4020875,46.720364062499996,30.4020875Z" fillRule="evenodd" fill="#FFFFFF" fillOpacity="1" transform="matrix(-1,0,0,-1,79.173828125,36.984375)" /></g></svg>
</button> </button>

View File

@ -26,6 +26,7 @@
aspect-ratio: 680 / 800; aspect-ratio: 680 / 800;
} }
} }
.images img { .images img {
width: 100%; width: 100%;
aspect-ratio: 680 / 800; aspect-ratio: 680 / 800;
@ -36,20 +37,25 @@
.images .imageItem { .images .imageItem {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
.imageOverlay { .imageOverlay {
background: transparent; background: transparent;
} }
} }
.images .imageItem:hover .imageOverlay { .images .imageItem:hover .imageOverlay {
top: 0; top: 0;
background: rgba(20, 53, 92, 0.8); background: rgba(20, 53, 92, 0.8);
.imageOverlayDesc { .imageOverlayDesc {
opacity: 1; opacity: 1;
} }
.imageOverlayTitle span { .imageOverlayTitle span {
border-bottom: 3px solid #FFFFFF; border-bottom: 3px solid #FFFFFF;
} }
} }
.images .imageMask { .images .imageMask {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -58,6 +64,7 @@
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
.images .imageOverlay { .images .imageOverlay {
position: absolute; position: absolute;
top: 78%; top: 78%;
@ -67,10 +74,12 @@
color: #fff; color: #fff;
padding: 3.75rem 2.5rem; padding: 3.75rem 2.5rem;
transition: top 0.3s ease-in-out; transition: top 0.3s ease-in-out;
.imageOverlayDesc { .imageOverlayDesc {
opacity: 0; opacity: 0;
} }
} }
.images .imageOverlayTitle { .images .imageOverlayTitle {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 500; font-weight: 500;
@ -79,11 +88,13 @@
text-align: center; text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.3); border-bottom: 1px solid rgba(255, 255, 255, 0.3);
} }
.images .imageOverlayTitle span { .images .imageOverlayTitle span {
display: inline-block; display: inline-block;
height: 4.4375rem; height: 4.4375rem;
border-bottom: 3px solid #14355C; border-bottom: 3px solid #14355C;
} }
.images .imageOverlayDesc { .images .imageOverlayDesc {
font-size: 1rem; font-size: 1rem;
color: #fff; color: #fff;
@ -94,6 +105,7 @@
padding: 6.25rem 0 9.375rem; padding: 6.25rem 0 9.375rem;
min-height: 100vh; min-height: 100vh;
} }
.founderIntroduction h2 { .founderIntroduction h2 {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
@ -101,6 +113,7 @@
margin-bottom: 3.125rem; margin-bottom: 3.125rem;
text-align: center; text-align: center;
} }
.founderIntroduction p { .founderIntroduction p {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.5; line-height: 1.5;
@ -109,6 +122,7 @@
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-align: center; text-align: center;
} }
.founderPhoto { .founderPhoto {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -121,10 +135,12 @@
width: 100%; width: 100%;
aspect-ratio: 680 / 800; aspect-ratio: 680 / 800;
} }
.founderPhotoContent { .founderPhotoContent {
padding-top: 6.25rem; padding-top: 6.25rem;
padding-right: 6.25rem; padding-right: 6.25rem;
} }
.founderPhotoContent p { .founderPhotoContent p {
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
@ -133,6 +149,28 @@
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.section3Content {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: auto;
align-items: start;
gap: 16px;
}
.section3Item {
width: 100%;
height: 100%;
color: #fff;
li {
font-weight: 500;
font-size: 16px;
color: #FFFFFF;
line-height: 22px;
list-style: disc;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.section { .section {
padding: 2rem 1rem; padding: 2rem 1rem;

View File

@ -2,6 +2,7 @@ import Banner from "@/components/Banner";
import styles from "./Founder.module.css"; import styles from "./Founder.module.css";
import ParagraphSection from "@/components/layout/ParagraphSection"; import ParagraphSection from "@/components/layout/ParagraphSection";
import { useStore } from "@/store"; import { useStore } from "@/store";
import Section from "@/components/layout/Section";
export default function AboutFounder() { export default function AboutFounder() {
const appConfig = useStore((s) => s.appConfig); const appConfig = useStore((s) => s.appConfig);
@ -10,6 +11,8 @@ export default function AboutFounder() {
const banner = founder?.banner; const banner = founder?.banner;
const section1Data = founder?.section1Data; const section1Data = founder?.section1Data;
const section2Data = founder?.section2Data; const section2Data = founder?.section2Data;
const section3Data = founder?.section3Data;
const section4Data = founder?.section4Data;
if (!founder) return null; if (!founder) return null;
@ -63,6 +66,26 @@ export default function AboutFounder() {
</div> </div>
</section> </section>
)} )}
{/* 社会职务 */}
<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])),
}).flatMap((_, rowIndex) =>
(section3Data?.columns ?? []).map((colItems, colIndex) => (
<div key={`${rowIndex}-${colIndex}`} className={styles.section3Item}>
{colItems[rowIndex] ? <li>{colItems[rowIndex].title}</li> : null}
</div>
))
)}
</div>
</Section>
{/* 荣誉奖项 */}
<Section title={section4Data?.title} background={section4Data?.backgroundImage ?? ""}>
</Section>
</div> </div>
); );
} }

View File

@ -60,7 +60,7 @@ export default function BusinessCommercialGroup() {
) : ( ) : (
<img <img
src={tabItems[activeTabIndex]?.image} src={tabItems[activeTabIndex]?.image}
alt="in77 杭州湖滨银泰" alt=""
onError={() => setIn77ImgError(true)} onError={() => setIn77ImgError(true)}
style={{ width: "100%", height: "800px" }} style={{ width: "100%", height: "800px" }}
/> />

View File

@ -2,27 +2,36 @@ import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import type mockData from "@/api/mockData"; import type mockData from "@/api/mockData";
import type { LocaleKey, SupportLocale } from "@/type";
export type AppConfig = typeof mockData extends { zhCN: infer Z } ? Z : never; export type AppConfig = typeof mockData extends { "zh-CN": infer Z } ? Z : never;
export type I18nData = { zhCN: AppConfig; en: AppConfig }; export type I18nData = { "zh-CN": AppConfig; "en-US": AppConfig };
export type LocaleKey = "zhCN" | "en";
/** 根据 navigator.language 映射到支持的 LocaleKey */
function getLocaleFromNavigator(): LocaleKey {
const lang = navigator.language as LocaleKey;
return lang;
}
interface StoreState { interface StoreState {
locale: LocaleKey; locale: LocaleKey;
i18nData: I18nData | null; i18nData: I18nData | null;
appConfig: AppConfig | null; appConfig: AppConfig | null;
supportLocales: SupportLocale[];
token: string | null; token: string | null;
setLocale: (locale: LocaleKey) => void; setLocale: (locale: LocaleKey) => void;
setAppConfig: (data: I18nData) => void; setAppConfig: (data: I18nData) => void;
setToken: (token: string | null) => void; setToken: (token: string | null) => void;
setSupportLocales: (locales: SupportLocale[]) => void;
} }
export const useStore = create<StoreState>()( export const useStore = create<StoreState>()(
persist( persist(
(set) => ({ (set) => ({
locale: "zhCN", locale: 'zh-CN',
i18nData: null, i18nData: null,
appConfig: null, appConfig: null,
supportLocales: [],
token: null, token: null,
setLocale: (locale) => setLocale: (locale) =>
set((state) => ({ set((state) => ({
@ -32,21 +41,28 @@ export const useStore = create<StoreState>()(
setAppConfig: (data) => setAppConfig: (data) =>
set((state) => ({ set((state) => ({
i18nData: data, i18nData: data,
appConfig: data[state.locale] ?? null, appConfig: data[state.locale] ?? data['en-US'] ?? data['zh-CN'] ?? null,
})), })),
setToken: (token) => set({ token }), setToken: (token) => set({ token }),
setSupportLocales: (locales: SupportLocale[]) => set({ supportLocales: locales }),
}), }),
{ {
name: "yintai-store", name: "yintai-store",
version: 1, version: 1,
migrate: (persisted: unknown) => { migrate: (persisted: unknown) => {
const p = persisted as Record<string, unknown>; const p = persisted as Record<string, unknown>;
if (p?.i18nData || !p?.appConfig) return p; const validLocales: LocaleKey[] = ["zh-CN", "en-US"];
const locale = validLocales.includes(p?.locale as LocaleKey)
? (p.locale as LocaleKey)
: getLocaleFromNavigator();
if (p?.i18nData || !p?.appConfig) {
return { ...p, locale };
}
const legacy = p.appConfig as AppConfig; const legacy = p.appConfig as AppConfig;
return { return {
...p, ...p,
locale: (p.locale as LocaleKey) ?? "zhCN", locale,
i18nData: { zhCN: legacy, en: legacy }, i18nData: { "zh-CN": legacy, "en-US": legacy },
appConfig: legacy, appConfig: legacy,
}; };
}, },

25
src/type/index.ts Normal file
View File

@ -0,0 +1,25 @@
/**
*
*/
// === 导航 & 路由 ===
export type NavChild = { path: string; label: string };
export type NavItem = { path: string; label: string; children?: NavChild[]; index?: boolean };
// === 国际化 ===
export type LocaleKey = "zh-CN" | "en-US";
export type SupportLocale = { key: LocaleKey; label: string };
// === 页面配置 ===
export type BannerConfig = {
title?: string;
content?: string;
subtitle?: string;
largeContent?: string;
titleSize?: "large" | "medium" | string;
showBreadcrumb?: boolean;
backgroundImage?: string | string[];
};
// === 从 store 导出(依赖 mockData===
export type { AppConfig, I18nData } from "@/store";