manual save(2025-12-29 00:16)

This commit is contained in:
SiteAgent Bot
2025-12-29 00:16:45 +08:00
parent b776315e27
commit e604e821e4
23 changed files with 3933 additions and 137 deletions

View File

@@ -11,6 +11,8 @@
},
"dependencies": {
"axios": "^1.13.2",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.11.0"

12
public/images/hero-bg.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="600" viewBox="0 0 1920 600">
<defs>
<linearGradient id="heroGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e3a8a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
</defs>
<rect width="1920" height="600" fill="url(#heroGrad)"/>
<circle cx="1600" cy="150" r="200" fill="#d4af37" opacity="0.1"/>
<circle cx="200" cy="500" r="150" fill="#d4af37" opacity="0.08"/>
<rect x="100" y="200" width="3" height="200" fill="#d4af37" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 589 B

5
public/images/logo.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60" viewBox="0 0 200 60">
<rect width="200" height="60" fill="#1e3a8a"/>
<text x="100" y="38" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="#d4af37" text-anchor="middle">诚裕集团</text>
<text x="100" y="52" font-family="Arial, sans-serif" font-size="10" fill="#ffffff" text-anchor="middle" opacity="0.8">CHENGYU GROUP</text>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect width="400" height="300" fill="#f1f5f9"/>
<rect x="50" y="50" width="300" height="200" rx="8" fill="#1e3a8a" opacity="0.1"/>
<path d="M150 150 L200 100 L250 150 L200 200 Z" fill="#d4af37" opacity="0.6"/>
<rect x="100" y="220" width="200" height="8" rx="2" fill="#1e3a8a" opacity="0.3"/>
<rect x="120" y="235" width="160" height="6" rx="2" fill="#94a3b8" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="250" viewBox="0 0 400 250">
<rect width="400" height="250" fill="#e2e8f0"/>
<rect x="140" y="75" width="120" height="100" rx="4" fill="#1e3a8a" opacity="0.15"/>
<rect x="160" y="95" width="80" height="60" rx="2" fill="#d4af37" opacity="0.2"/>
<rect x="150" y="180" width="100" height="12" rx="2" fill="#1e3a8a" opacity="0.4"/>
<rect x="150" y="200" width="150" height="8" rx="2" fill="#94a3b8" opacity="0.5"/>
<rect x="150" y="215" width="120" height="8" rx="2" fill="#94a3b8" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">
<rect width="800" height="400" fill="#f8fafc"/>
<rect x="50" y="50" width="700" height="300" rx="8" fill="#1e3a8a" opacity="0.1"/>
<rect x="100" y="100" width="200" height="120" rx="4" fill="#1e3a8a" opacity="0.2"/>
<rect x="320" y="100" width="200" height="120" rx="4" fill="#d4af37" opacity="0.2"/>
<rect x="540" y="100" width="160" height="120" rx="4" fill="#1e3a8a" opacity="0.15"/>
<rect x="100" y="240" width="600" height="8" rx="2" fill="#d4af37" opacity="0.3"/>
<rect x="100" y="260" width="500" height="8" rx="2" fill="#94a3b8" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 662 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect width="400" height="300" fill="#f1f5f9"/>
<rect x="150" y="80" width="100" height="100" rx="50" fill="#1e3a8a" opacity="0.2"/>
<circle cx="200" cy="130" r="40" fill="#1e3a8a" opacity="0.3"/>
<rect x="160" y="180" width="80" height="10" rx="2" fill="#d4af37" opacity="0.5"/>
<rect x="170" y="200" width="60" height="6" rx="2" fill="#94a3b8" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -1,18 +1,78 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { Home } from './pages/Home'
import { PostDetail } from './pages/PostDetail'
import { CategoriesPage } from './pages/Categories'
import { CategoryDetail } from './pages/CategoryDetail'
import { About } from './pages/About'
import { Services } from './pages/Services'
import { News } from './pages/News'
import { Contact } from './pages/Contact'
// 页面切换动画配置
const pageVariants = {
initial: {
opacity: 0,
y: 20,
},
animate: {
opacity: 1,
y: 0,
},
exit: {
opacity: 0,
y: -20,
},
}
const pageTransition = {
duration: 0.4,
ease: 'easeInOut',
}
// 页面包装组件 - 添加动画效果
const PageWrapper = ({ children }: { children: React.ReactNode }) => (
<motion.div
initial="initial"
animate="animate"
exit="exit"
variants={pageVariants}
transition={pageTransition}
style={{ width: '100%' }}
>
{children}
</motion.div>
)
// 404 页面组件
const NotFound = () => (
<PageWrapper>
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center">
<h1 className="text-9xl font-bold text-primary">404</h1>
<p className="text-2xl font-medium text-gray-600 mt-4"></p>
<p className="text-gray-500 mt-2">访</p>
<a
href="/"
className="inline-block mt-8 px-8 py-3 bg-primary text-white rounded-lg hover:bg-primary-light transition-colors"
>
</a>
</div>
</div>
</PageWrapper>
)
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:slug" element={<PostDetail />} />
<Route path="/categories" element={<CategoriesPage />} />
<Route path="/categories/:slug" element={<CategoryDetail />} />
</Routes>
<AnimatePresence mode="wait">
<Routes>
<Route path="/" element={<PageWrapper><Home /></PageWrapper>} />
<Route path="/about" element={<PageWrapper><About /></PageWrapper>} />
<Route path="/services" element={<PageWrapper><Services /></PageWrapper>} />
<Route path="/news" element={<PageWrapper><News /></PageWrapper>} />
<Route path="/contact" element={<PageWrapper><Contact /></PageWrapper>} />
<Route path="*" element={<NotFound />} />
</Routes>
</AnimatePresence>
</Router>
)
}

View File

@@ -1,14 +1,250 @@
import React from 'react'
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
Building2,
Phone,
Mail,
MapPin,
Clock,
Wechat,
Weibo,
Linkedin,
ArrowRight,
} from 'lucide-react';
import {
COMPANY_INFO,
FOOTER_LINKS,
SOCIAL_MEDIA,
} from '../../lib/constants';
/**
* Footer 组件 - 企业官网页脚
*/
// 动画变体配置
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
};
// 图标映射
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
Wechat,
Weibo,
Linkedin,
};
/**
* 联系方式项组件
*/
const ContactItem: React.FC<{
icon: React.ComponentType<{ size?: number; className?: string }>;
title: string;
content: string;
}> = ({ icon: Icon, title, content }) => (
<div className="flex items-start gap-3">
<Icon size={18} className="text-accent mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-gray-400 mb-0.5">{title}</p>
<p className="text-sm text-gray-200">{content}</p>
</div>
</div>
);
/**
* 链接列组件
*/
const LinkColumn: React.FC<{
title: string;
links: Array<{ label: string; path: string }>;
}> = ({ title, links }) => (
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">
{title}
</h3>
<ul className="space-y-3">
{links.map((link) => (
<li key={link.path}>
<Link
to={link.path}
className="flex items-center gap-2 text-sm text-gray-300 hover:text-accent transition-colors duration-200 group"
>
<ArrowRight
size={14}
className="opacity-0 -ml-4 group-hover:opacity-100 group-hover:ml-0 transition-all duration-200"
/>
<span className="group-hover:translate-x-1 transition-transform duration-200">
{link.label}
</span>
</Link>
</li>
))}
</ul>
</div>
);
/**
* Footer 组件
*/
export const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-gray-50 border-t border-gray-200 py-8 mt-12">
<div className="container mx-auto px-4 text-center text-gray-600">
<p>Powered by TenantCMS</p>
<p className="text-sm mt-2">
Using X-Tenant-Slug for multi-tenant authentication
</p>
<footer className="bg-primary-dark text-white" role="contentinfo">
{/* 主内容区域 */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 lg:gap-12"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{/* 企业信息 */}
<motion.div className="lg:col-span-2 space-y-6" variants={itemVariants}>
{/* Logo */}
<Link to="/" className="flex items-center gap-3 group" aria-label="返回首页">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-accent text-primary-dark">
<Building2 size={28} />
</div>
<div>
<span className="text-xl font-bold text-white group-hover:text-accent transition-colors">
</span>
<p className="text-xs text-gray-400">Chengyu Group</p>
</div>
</Link>
{/* 企业简介 */}
<p className="text-sm text-gray-300 leading-relaxed max-w-sm">
{COMPANY_INFO.description}
</p>
{/* 联系方式 */}
<div className="space-y-3 pt-2">
<ContactItem
icon={MapPin}
title="总部地址"
content={COMPANY_INFO.headquarters}
/>
<ContactItem
icon={Phone}
title="服务热线"
content={COMPANY_INFO.phone}
/>
<ContactItem
icon={Mail}
title="商务邮箱"
content={COMPANY_INFO.email}
/>
<ContactItem
icon={Clock}
title="工作时间"
content={COMPANY_INFO.workingHours}
/>
</div>
</motion.div>
{/* 产品服务 */}
<motion.div variants={itemVariants}>
<LinkColumn title="产品服务" links={FOOTER_LINKS.products} />
</motion.div>
{/* 公司信息 */}
<motion.div variants={itemVariants}>
<LinkColumn title="关于我们" links={FOOTER_LINKS.company} />
</motion.div>
{/* 社交媒体 */}
<motion.div variants={itemVariants}>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">
</h3>
<p className="text-xs text-gray-400 mb-4">
</p>
<div className="flex flex-wrap gap-3">
{SOCIAL_MEDIA.map((social) => {
const Icon = iconMap[social.icon] || Wechat;
return (
<motion.a
key={social.id}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 bg-white/10 rounded-lg text-sm text-gray-300 hover:bg-accent hover:text-primary-dark transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
aria-label={social.label}
>
<Icon size={18} />
<span className="hidden sm:inline">{social.label}</span>
</motion.a>
);
})}
</div>
</motion.div>
</motion.div>
</div>
{/* 底部版权栏 */}
<div className="border-t border-white/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
{/* 版权信息 */}
<motion.p
className="text-sm text-gray-400"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
>
© {currentYear} {COMPANY_INFO.fullName}
</motion.p>
{/* 备案和链接 */}
<motion.div
className="flex flex-wrap items-center justify-center gap-4 text-sm text-gray-400"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
>
<span>ICP备XXXXXXXX号</span>
<span className="hidden sm:inline">|</span>
<Link
to="/privacy"
className="hover:text-accent transition-colors duration-200"
>
</Link>
<span className="hidden sm:inline">|</span>
<Link
to="/terms"
className="hover:text-accent transition-colors duration-200"
>
使
</Link>
</motion.div>
</div>
</div>
</div>
</footer>
)
}
);
};
export default Footer;

View File

@@ -1,19 +1,177 @@
import React from 'react'
import { useState, useEffect, useCallback } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X, Building2 } from 'lucide-react';
import { NAVIGATION_MENU } from '../../lib/constants';
/**
* Header 组件 - 企业官网导航栏
*/
export const Header: React.FC = () => {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const location = useLocation();
// 监听滚动事件
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// 关闭移动端菜单 - 使用回调避免直接 setState
const closeMobileMenu = useCallback(() => {
setIsMobileMenuOpen(false);
}, []);
useEffect(() => {
closeMobileMenu();
}, [location.pathname, closeMobileMenu]);
// 检查当前路径是否为活动状态
const isActive = (path: string): boolean => {
if (path === '/') {
return location.pathname === '/';
}
return location.pathname.startsWith(path);
};
return (
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">
TenantCMS <span className="text-blue-600">Demo</span>
</h1>
<nav className="flex items-center gap-4">
<a href="/" className="text-gray-600 hover:text-gray-900"></a>
<a href="/categories" className="text-gray-600 hover:text-gray-900"></a>
<motion.header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled
? 'bg-white/95 backdrop-blur-sm shadow-md'
: 'bg-white shadow-sm'
}`}
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-[70px]">
{/* Logo 区域 */}
<Link
to="/"
className="flex items-center gap-3 group"
aria-label="返回首页"
>
<motion.div
className="flex items-center justify-center w-10 h-10 rounded-lg bg-primary text-white"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Building2 size={24} />
</motion.div>
<div className="hidden sm:block">
<span className="text-lg font-bold text-primary-dark group-hover:text-primary transition-colors">
</span>
<p className="text-xs text-gray-500 -mt-1">Chengyu Group</p>
</div>
</Link>
{/* 桌面端导航菜单 */}
<nav
className="hidden md:flex items-center gap-1"
role="navigation"
aria-label="主导航"
>
{NAVIGATION_MENU.map((item) => (
<Link
key={item.id}
to={item.path}
className={`relative px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
isActive(item.path)
? 'text-primary'
: 'text-gray-600 hover:text-primary'
}`}
aria-current={isActive(item.path) ? 'page' : undefined}
>
{item.label}
{/* 活动状态指示器 */}
{isActive(item.path) && (
<motion.div
layoutId="activeIndicator"
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-accent rounded-full"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
{/* 悬停效果 */}
{!isActive(item.path) && (
<span className="absolute inset-0 rounded-lg bg-primary/5 scale-0 transition-transform duration-200 group-hover:scale-100" />
)}
</Link>
))}
</nav>
{/* 桌面端 CTA 按钮 */}
<div className="hidden md:flex items-center gap-3">
<motion.button
className="px-5 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary-light transition-colors shadow-sm"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
{/* 移动端菜单按钮 */}
<motion.button
className="md:hidden p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label={isMobileMenuOpen ? '关闭菜单' : '打开菜单'}
aria-expanded={isMobileMenuOpen}
whileTap={{ scale: 0.95 }}
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
</div>
</div>
</header>
)
}
{/* 移动端菜单 */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.nav
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="md:hidden bg-white border-t border-gray-100 overflow-hidden"
role="navigation"
aria-label="移动端导航"
>
<div className="px-4 py-4 space-y-2">
{NAVIGATION_MENU.map((item) => (
<Link
key={item.id}
to={item.path}
className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
isActive(item.path)
? 'bg-primary/5 text-primary'
: 'text-gray-600 hover:bg-gray-50'
}`}
aria-current={isActive(item.path) ? 'page' : undefined}
>
{item.label}
</Link>
))}
<div className="pt-4 border-t border-gray-100">
<motion.button
className="w-full px-4 py-3 text-base font-medium text-white bg-primary rounded-lg"
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.nav>
)}
</AnimatePresence>
</motion.header>
);
};
export default Header;

291
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,291 @@
import { useEffect, useRef } from 'react';
import { motion, useInView, useAnimation } from 'framer-motion';
import { ArrowDown, TrendingUp, Users, Building2, Award } from 'lucide-react';
import { COMPANY_INFO, COMPANY_STATS } from '../../lib/constants';
/**
* 数字递增动画组件
*/
const CountUpNumber: React.FC<{
value: string;
suffix?: string;
duration?: number;
}> = ({ value, suffix = '', duration = 2 }) => {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const controls = useAnimation();
useEffect(() => {
if (isInView && ref.current) {
const numValue = parseInt(value.replace(/[^0-9]/g, ''));
const hasPlus = value.includes('+');
const hasYi = value.includes('亿');
controls.start({
opacity: [0, 1],
scale: [0.5, 1.2, 1],
});
// 数字递增动画
let current = 0;
const increment = numValue / (duration * 60);
const timer = setInterval(() => {
current += increment;
if (current >= numValue) {
current = numValue;
clearInterval(timer);
}
let display = Math.floor(current);
if (hasYi) {
display = Math.floor(current);
}
ref.current.textContent = display.toLocaleString() + (hasPlus ? '+' : '') + (hasYi ? '亿' : suffix);
}, 1000 / 60);
return () => clearInterval(timer);
}
}, [isInView, value, suffix, duration, controls]);
return (
<motion.span
ref={ref}
className="text-3xl md:text-4xl font-bold text-accent"
initial={{ opacity: 0, scale: 0.5 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.5 }}
>
0
</motion.span>
);
};
/**
* 统计数据项组件
*/
const StatItem: React.FC<{
icon: React.ComponentType<{ size?: number; className?: string }>;
label: string;
value: string;
suffix?: string;
}> = ({ icon: Icon, label, value, suffix }) => (
<motion.div
className="flex items-center gap-4 p-4 bg-white/10 rounded-xl backdrop-blur-sm"
whileHover={{ scale: 1.02, backgroundColor: 'rgba(255,255,255,0.15)' }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-accent/20">
<Icon size={24} className="text-accent" />
</div>
<div>
<div className="flex items-baseline gap-1">
<CountUpNumber value={value} suffix={suffix} />
</div>
<p className="text-sm text-gray-300">{label}</p>
</div>
</motion.div>
);
/**
* Hero 组件 - 首页大图区域
*/
export const Hero: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const statsRef = useRef<HTMLDivElement>(null);
// 滚动到指定区域
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
}
};
return (
<section
ref={containerRef}
className="relative min-h-screen flex items-center overflow-hidden"
role="banner"
>
{/* 背景层 */}
<div className="absolute inset-0 z-0">
{/* 渐变背景 */}
<div className="absolute inset-0 bg-gradient-to-br from-primary-dark via-primary to-primary" />
{/* 装饰性图案 */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-0 right-0 w-[800px] h-[800px] rounded-full bg-white/5 blur-3xl -translate-y-1/2 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] rounded-full bg-accent/10 blur-3xl translate-y-1/2 -translate-x-1/2" />
</div>
{/* 网格图案 */}
<div
className="absolute inset-0 opacity-5"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
backgroundSize: '50px 50px',
}}
/>
</div>
{/* 主内容区域 */}
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* 左侧内容 */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
{/* 企业标语 */}
<motion.h1
className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white leading-tight"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
{COMPANY_INFO.slogan}
</motion.h1>
{/* 企业名称 */}
<motion.p
className="mt-6 text-2xl sm:text-3xl font-semibold text-accent"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
{COMPANY_INFO.fullName}
</motion.p>
{/* 企业简介 */}
<motion.p
className="mt-6 text-lg text-gray-200 leading-relaxed max-w-xl"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
{COMPANY_INFO.description}
</motion.p>
{/* CTA 按钮组 */}
<motion.div
className="mt-8 flex flex-wrap gap-4"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
>
<motion.button
onClick={() => scrollToSection('about')}
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-primary-dark font-semibold rounded-lg shadow-lg shadow-accent/25 hover:shadow-xl hover:shadow-accent/30 transition-all duration-300"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
>
<ArrowDown size={20} />
</motion.button>
<motion.button
onClick={() => scrollToSection('contact')}
className="inline-flex items-center gap-2 px-8 py-4 border-2 border-white/30 text-white font-semibold rounded-lg hover:bg-white/10 transition-all duration-300"
whileHover={{ scale: 1.05, borderColor: 'rgba(255,255,255,0.5)' }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</motion.div>
</motion.div>
{/* 右侧统计数据 */}
<motion.div
ref={statsRef}
className="space-y-4"
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, duration: 0.8, ease: 'easeOut' }}
>
<motion.h2
className="text-xl font-semibold text-white mb-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
</motion.h2>
<div className="grid grid-cols-2 gap-4">
{/* 成立年限 */}
<StatItem
icon={Award}
label={COMPANY_STATS[0].label}
value={COMPANY_STATS[0].value}
suffix={COMPANY_STATS[0].suffix}
/>
{/* 员工数量 */}
<StatItem
icon={Users}
label={COMPANY_STATS[1].label}
value={COMPANY_STATS[1].value}
suffix={COMPANY_STATS[1].suffix}
/>
{/* 服务客户 */}
<StatItem
icon={Building2}
label={COMPANY_STATS[2].label}
value={COMPANY_STATS[2].value}
suffix={COMPANY_STATS[2].suffix}
/>
{/* 管理资产 */}
<StatItem
icon={TrendingUp}
label={COMPANY_STATS[3].label}
value={COMPANY_STATS[3].value}
suffix={COMPANY_STATS[3].suffix}
/>
</div>
{/* 额外说明 */}
<motion.p
className="mt-6 text-sm text-gray-400 text-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
2025 12
</motion.p>
</motion.div>
</div>
</div>
{/* 底部装饰 */}
<motion.div
className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-background to-transparent"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2 }}
/>
{/* 滚动提示 */}
<motion.div
className="absolute bottom-8 left-1/2 -translate-x-1/2"
initial={{ opacity: 0 }}
animate={{ opacity: 1, y: [0, 10, 0] }}
transition={{ delay: 1.5, duration: 2, repeat: Infinity }}
>
<button
onClick={() => scrollToSection('about')}
className="flex flex-col items-center gap-2 text-white/60 hover:text-accent transition-colors duration-300"
aria-label="向下滚动"
>
<span className="text-xs uppercase tracking-wider">Scroll</span>
<ArrowDown size={20} />
</button>
</motion.div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,187 @@
import { motion } from 'framer-motion';
import { Target, Eye, Heart, Award, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
/**
* AboutSection 组件 - 关于我们简介区域
*/
const values = [
{
icon: Heart,
title: '诚信为本',
description: '坚守诚信底线,建立长期信任关系',
},
{
icon: Target,
title: '创新驱动',
description: '持续创新,保持行业领先优势',
},
{
icon: Award,
title: '品质至上',
description: '追求卓越,提供高品质服务',
},
];
export const AboutSection: React.FC = () => {
return (
<section
id="about"
className="py-20 lg:py-28 bg-background"
aria-labelledby="about-heading"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* 标题区域 */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2
id="about-heading"
className="text-3xl md:text-4xl font-bold text-primary-dark"
>
</h2>
<p className="mt-4 text-lg text-gray-600 max-w-3xl mx-auto">
2010
</p>
</motion.div>
{/* 主要内容区域 */}
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* 左侧内容 */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h3 className="text-2xl font-semibold text-primary-dark mb-6">
·
</h3>
<div className="space-y-6 text-gray-600 leading-relaxed">
<p>
"诚信、创新、共赢"
</p>
<p>
1000
</p>
</div>
{/* 使命与愿景 */}
<div className="mt-8 grid sm:grid-cols-2 gap-6">
<div className="p-5 bg-white rounded-xl shadow-sm border border-gray-100">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-primary/10 rounded-lg">
<Eye size={20} className="text-primary" />
</div>
<h4 className="font-semibold text-primary-dark"></h4>
</div>
<p className="text-sm text-gray-600">
</p>
</div>
<div className="p-5 bg-white rounded-xl shadow-sm border border-gray-100">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-accent/20 rounded-lg">
<Target size={20} className="text-accent" />
</div>
<h4 className="font-semibold text-primary-dark">使</h4>
</div>
<p className="text-sm text-gray-600">
</p>
</div>
</div>
{/* 更多按钮 */}
<motion.div className="mt-8" whileHover={{ x: 5 }}>
<Link
to="/about"
className="inline-flex items-center gap-2 text-primary font-medium hover:text-primary-light transition-colors"
>
<ArrowRight size={18} />
</Link>
</motion.div>
</motion.div>
{/* 右侧图片/装饰 */}
<motion.div
className="relative"
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* 主图片区域 */}
<div className="aspect-[4/3] rounded-2xl overflow-hidden bg-gradient-to-br from-primary to-primary-light shadow-xl">
<div className="w-full h-full flex items-center justify-center">
<div className="text-center text-white p-8">
<div className="text-6xl font-bold mb-2">15+</div>
<div className="text-lg opacity-80"></div>
</div>
</div>
</div>
{/* 装饰性元素 */}
<motion.div
className="absolute -bottom-6 -left-6 w-32 h-32 bg-accent/20 rounded-2xl -z-10"
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.4, duration: 0.5 }}
/>
<motion.div
className="absolute -top-6 -right-6 w-24 h-24 bg-primary/10 rounded-2xl -z-10"
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.5, duration: 0.5 }}
/>
</motion.div>
</div>
{/* 核心价值观 */}
<motion.div
className="mt-20"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h3 className="text-2xl font-semibold text-center text-primary-dark mb-10">
</h3>
<div className="grid md:grid-cols-3 gap-8">
{values.map((value, index) => (
<motion.div
key={value.title}
className="text-center p-8 bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/10 mb-5">
<value.icon size={32} className="text-primary" />
</div>
<h4 className="text-xl font-semibold text-primary-dark mb-3">
{value.title}
</h4>
<p className="text-gray-600">{value.description}</p>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default AboutSection;

View File

@@ -0,0 +1,172 @@
import { motion } from 'framer-motion';
import { Calendar, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { formatDate } from '../../lib/utils';
/**
* NewsSection 组件 - 最新动态区域
*/
// 模拟新闻数据
const newsItems = [
{
id: 1,
category: 'company',
title: '诚裕集团荣获"2025年度优秀企业"称号',
excerpt: '在近日举办的年度企业评选活动中,诚裕集团凭借其卓越的经营业绩和社会责任表现,荣获"2025年度优秀企业"称号。',
date: '2025-12-20',
image: null,
},
{
id: 2,
category: 'industry',
title: '金融科技创新论坛圆满落幕,诚裕集团分享行业洞察',
excerpt: '诚裕集团受邀参加金融科技创新论坛,与行业专家共同探讨金融科技发展趋势,分享公司在数字化转型方面的实践经验。',
date: '2025-12-15',
image: null,
},
{
id: 3,
category: 'achievement',
title: '诚裕集团完成新一轮战略融资,估值突破百亿',
excerpt: '诚裕集团宣布完成新一轮战略融资,本轮融资由知名投资机构领投,估值突破百亿元人民币,标志着公司发展进入新阶段。',
date: '2025-12-10',
image: null,
},
];
// 新闻分类映射
const categoryMap: Record<string, { label: string; color: string }> = {
company: { label: '公司动态', color: 'bg-primary/10 text-primary' },
industry: { label: '行业资讯', color: 'bg-accent/20 text-accent-dark' },
achievement: { label: '荣誉资质', color: 'bg-green-100 text-green-700' },
};
/**
* 新闻卡片组件
*/
const NewsCard: React.FC<{
news: typeof newsItems[0];
index: number;
}> = ({ news, index }) => {
const category = categoryMap[news.category] || categoryMap.company;
return (
<motion.article
className="group bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-lg transition-shadow duration-300"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
{/* 图片区域(占位) */}
<div className="aspect-[16/9] bg-gradient-to-br from-primary/5 to-primary-light/10 flex items-center justify-center relative overflow-hidden">
<div className="text-primary/20">
<svg
className="w-16 h-16"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
</div>
{/* 分类标签 */}
<span
className={`absolute top-4 left-4 px-3 py-1 text-xs font-medium rounded-full ${category.color}`}
>
{category.label}
</span>
</div>
{/* 内容区域 */}
<div className="p-6">
{/* 日期 */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-3">
<Calendar size={14} />
<time dateTime={news.date}>{formatDate(news.date, 'YYYY年MM月DD日')}</time>
</div>
{/* 标题 */}
<h3 className="text-lg font-semibold text-primary-dark mb-3 line-clamp-2 group-hover:text-primary transition-colors">
{news.title}
</h3>
{/* 摘要 */}
<p className="text-gray-600 text-sm leading-relaxed line-clamp-3 mb-4">
{news.excerpt}
</p>
{/* 了解更多链接 */}
<Link
to={`/news/${news.id}`}
className="inline-flex items-center gap-2 text-sm font-medium text-primary group-hover:text-primary-light transition-colors"
>
<motion.span
initial={{ x: 0 }}
whileHover={{ x: 5 }}
transition={{ duration: 0.2 }}
>
<ArrowRight size={16} />
</motion.span>
</Link>
</div>
</motion.article>
);
};
export const NewsSection: React.FC = () => {
return (
<section
id="news"
className="py-20 lg:py-28 bg-background"
aria-labelledby="news-heading"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* 标题区域 */}
<motion.div
className="flex flex-col md:flex-row md:items-end md:justify-between gap-4 mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div>
<h2
id="news-heading"
className="text-3xl md:text-4xl font-bold text-primary-dark"
>
</h2>
<p className="mt-2 text-lg text-gray-600">
</p>
</div>
<Link
to="/news"
className="inline-flex items-center gap-2 text-primary font-medium hover:text-primary-light transition-colors"
>
<ArrowRight size={18} />
</Link>
</motion.div>
{/* 新闻卡片网格 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{newsItems.map((news, index) => (
<NewsCard key={news.id} news={news} index={index} />
))}
</div>
</div>
</section>
);
};
export default NewsSection;

View File

@@ -0,0 +1,135 @@
import { motion } from 'framer-motion';
import { TrendingUp, Cpu, Building2, Briefcase, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { SERVICES } from '../../lib/constants';
/**
* ServicesSection 组件 - 核心业务展示区域
*/
// 业务卡片组件
const ServiceCard: React.FC<{
service: typeof SERVICES[0];
index: number;
}> = ({ service, index }) => {
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
TrendingUp,
Cpu,
Building2,
Briefcase,
};
const Icon = iconMap[service.icon] || TrendingUp;
return (
<motion.div
className="group relative p-8 bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -8, shadow: '0 20px 40px rgba(0,0,0,0.1)' }}
>
{/* 背景装饰 */}
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-primary/5 to-accent/5 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
{/* 图标 */}
<div className="relative z-10 inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-primary-light text-white mb-6 group-hover:scale-110 transition-transform duration-300">
<Icon size={32} />
</div>
{/* 内容 */}
<h3 className="relative z-10 text-xl font-semibold text-primary-dark mb-4">
{service.title}
</h3>
<p className="relative z-10 text-gray-600 leading-relaxed mb-6">
{service.description}
</p>
{/* 特性列表 */}
<ul className="relative z-10 space-y-3 mb-8">
{service.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-600">
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
{feature}
</li>
))}
</ul>
{/* 了解更多链接 */}
<Link
to={`/services/${service.id}`}
className="relative z-10 inline-flex items-center gap-2 text-primary font-medium group-hover:text-primary-light transition-colors"
>
<motion.span
initial={{ x: 0 }}
whileHover={{ x: 5 }}
transition={{ duration: 0.2 }}
>
<ArrowRight size={18} />
</motion.span>
</Link>
{/* 悬停边框效果 */}
<div className="absolute inset-0 border-2 border-transparent group-hover:border-accent/30 rounded-2xl transition-colors duration-300" />
</motion.div>
);
};
export const ServicesSection: React.FC = () => {
return (
<section
id="services"
className="py-20 lg:py-28 bg-white"
aria-labelledby="services-heading"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* 标题区域 */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2
id="services-heading"
className="text-3xl md:text-4xl font-bold text-primary-dark"
>
</h2>
<p className="mt-4 text-lg text-gray-600 max-w-3xl mx-auto">
</p>
</motion.div>
{/* 业务卡片网格 */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{SERVICES.map((service, index) => (
<ServiceCard key={service.id} service={service} index={index} />
))}
</div>
{/* 查看全部服务 */}
<motion.div
className="mt-12 text-center"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<Link
to="/services"
className="inline-flex items-center gap-2 px-8 py-4 bg-primary text-white font-medium rounded-lg hover:bg-primary-light transition-colors shadow-lg shadow-primary/25"
>
<ArrowRight size={20} />
</Link>
</motion.div>
</div>
</section>
);
};
export default ServicesSection;

View File

@@ -2,7 +2,44 @@
@plugin "@tailwindcss/typography";
@theme {
--color-primary: #1e3a8a;
--color-primary-light: #2563eb;
--color-primary-dark: #1e293b;
--color-accent: #d4af37;
--color-accent-light: #e5c158;
--color-accent-dark: #b8960c;
--color-background: #f8fafc;
--color-background-light: #ffffff;
--color-background-dark: #f1f5f9;
}
body {
margin: 0;
min-height: 100vh;
background-color: var(--color-background);
color: var(--color-primary-dark);
font-family: 'Inter', system-ui, sans-serif;
}
html {
scroll-behavior: smooth;
}
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background-dark);
}
::-webkit-scrollbar-thumb {
background: var(--color-primary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-primary-light);
}

177
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,177 @@
/**
* 诚裕集团企业官网 - 常量定义
*/
// 企业基本信息
export const COMPANY_INFO = {
name: '诚裕集团',
nameEn: 'Chengyu Group',
slogan: '诚信为本,裕及四方',
description: '诚裕集团成立于2010年是一家集科技研发、金融服务、产业投资于一体的综合性企业集团。秉承"诚信、创新、共赢"的经营理念,致力于为客户提供高品质的产品和服务。',
fullName: '诚裕集团有限公司',
registrationNumber: '91110000XXXXXXXX',
established: '2010年',
headquarters: '北京市朝阳区建国路88号',
phone: '400-888-8888',
email: 'contact@chengyu-group.com',
workingHours: '周一至周五 9:00-18:00',
};
// 导航菜单配置
export const NAVIGATION_MENU = [
{ id: 'home', label: '首页', path: '/' },
{ id: 'about', label: '关于我们', path: '/about' },
{ id: 'services', label: '产品服务', path: '/services' },
{ id: 'news', label: '新闻资讯', path: '/news' },
{ id: 'contact', label: '联系我们', path: '/contact' },
];
// 底部导航链接
export const FOOTER_LINKS = {
products: [
{ label: '金融服务', path: '/services/finance' },
{ label: '科技研发', path: '/services/tech' },
{ label: '产业投资', path: '/services/investment' },
{ label: '咨询服务', path: '/services/consulting' },
],
company: [
{ label: '关于我们', path: '/about' },
{ label: '新闻资讯', path: '/news' },
{ label: '招贤纳士', path: '/careers' },
{ label: '联系我们', path: '/contact' },
],
legal: [
{ label: '隐私政策', path: '/privacy' },
{ label: '使用条款', path: '/terms' },
{ label: '免责声明', path: '/disclaimer' },
],
};
// 社交媒体链接
export const SOCIAL_MEDIA = [
{
id: 'wechat',
label: '微信公众号',
icon: 'Wechat',
url: 'https://weixin.qq.com',
description: '诚裕集团官方微信公众号',
},
{
id: 'weibo',
label: '官方微博',
icon: 'Weibo',
url: 'https://weibo.com',
description: '诚裕集团官方微博账号',
},
{
id: 'linkedin',
label: 'LinkedIn',
icon: 'Linkedin',
url: 'https://linkedin.com/company/chengyu-group',
description: '诚裕集团 LinkedIn 主页',
},
];
// 服务项目配置
export const SERVICES = [
{
id: 'finance',
title: '金融服务',
description: '提供专业的财富管理、投资顾问、资产配置等金融服务,为客户创造稳健收益。',
icon: 'TrendingUp',
features: ['财富管理', '投资顾问', '资产配置', '风险管理'],
},
{
id: 'tech',
title: '科技研发',
description: '聚焦人工智能、大数据、云计算等前沿技术,为企业提供数字化转型解决方案。',
icon: 'Cpu',
features: ['人工智能', '大数据分析', '云计算服务', '数字化转型'],
},
{
id: 'investment',
title: '产业投资',
description: '专注于新兴产业投资机会,通过战略投资推动产业升级和价值创造。',
icon: 'Building2',
features: ['战略投资', '产业并购', '创业孵化', '退出管理'],
},
{
id: 'consulting',
title: '咨询服务',
description: '为企业提供战略规划、运营优化、风险管理等专业咨询服务。',
icon: 'Briefcase',
features: ['战略规划', '运营优化', '风险管理', '组织变革'],
},
];
// 新闻分类
export const NEWS_CATEGORIES = [
{ id: 'all', label: '全部' },
{ id: 'company', label: '公司动态' },
{ id: 'industry', label: '行业资讯' },
{ id: 'achievement', label: '荣誉资质' },
];
// 首页统计数据
export const COMPANY_STATS = [
{ id: 'years', label: '成立年限', value: '15', suffix: '年' },
{ id: 'employees', label: '员工数量', value: '500', suffix: '+' },
{ id: 'clients', label: '服务客户', value: '1000', suffix: '+' },
{ id: 'assets', label: '管理资产', value: '500', suffix: '亿' },
];
// 页面元信息
export const PAGE_META = {
home: {
title: '诚裕集团 - 诚信为本,裕及四方',
description: '诚裕集团是一家集科技研发、金融服务、产业投资于一体的综合性企业集团',
},
about: {
title: '关于我们 - 诚裕集团',
description: '了解诚裕集团的发展历程、企业文化和核心价值观',
},
services: {
title: '产品服务 - 诚裕集团',
description: '提供金融服务、科技研发、产业投资、咨询管理等专业服务',
},
news: {
title: '新闻资讯 - 诚裕集团',
description: '了解诚裕集团最新动态、行业资讯和荣誉资质',
},
contact: {
title: '联系我们 - 诚裕集团',
description: '获取诚裕集团联系方式,欢迎随时与我们沟通',
},
};
// 联系方式配置
export const CONTACT_INFO = [
{
id: 'address',
type: 'address',
icon: 'MapPin',
title: '总部地址',
content: COMPANY_INFO.headquarters,
},
{
id: 'phone',
type: 'phone',
icon: 'Phone',
title: '服务热线',
content: COMPANY_INFO.phone,
},
{
id: 'email',
type: 'email',
icon: 'Mail',
title: '商务邮箱',
content: COMPANY_INFO.email,
},
{
id: 'hours',
type: 'text',
icon: 'Clock',
title: '工作时间',
content: COMPANY_INFO.workingHours,
},
];

273
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,273 @@
/**
* 诚裕集团企业官网 - 工具函数
*/
/**
* 日期格式化函数
* @param dateString - ISO 日期字符串
* @param format - 格式化模板,默认为 'YYYY-MM-DD'
* @returns 格式化后的日期字符串
*/
export function formatDate(dateString: string, format: 'YYYY-MM-DD' | 'YYYY年MM月DD日' | 'MM/DD/YYYY' = 'YYYY-MM-DD'): string {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
switch (format) {
case 'YYYY年MM月DD日':
return `${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}`;
case 'MM/DD/YYYY':
return `${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')}/${year}`;
case 'YYYY-MM-DD':
default:
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
}
/**
* 文本截取函数(按字符数)
* @param text - 原始文本
* @param maxLength - 最大字符数
* @param suffix - 截取后添加的后缀,默认为 '...'
* @returns 截取后的文本
*/
export function truncateText(text: string, maxLength: number, suffix: string = '...'): string {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - suffix.length) + suffix;
}
/**
* 文本截取函数(按单词数,适用于英文)
* @param text - 原始文本
* @param maxWords - 最大单词数
* @param suffix - 截取后添加的后缀,默认为 '...'
* @returns 截取后的文本
*/
export function truncateWords(text: string, maxWords: number, suffix: string = '...'): string {
if (!text) return '';
const words = text.split(/\s+/);
if (words.length <= maxWords) return text;
return words.slice(0, maxWords).join(' ') + suffix;
}
/**
* 生成随机ID
* @param prefix - ID 前缀,默认为 'id'
* @returns 生成的随机 ID
*/
export function generateId(prefix: string = 'id'): string {
return `${prefix}-${Math.random().toString(36).substring(2, 11)}`;
}
/**
* 延迟函数
* @param ms - 延迟毫秒数
* @returns Promise
*/
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 滚动到指定元素
* @param elementId - 元素 ID
* @param offset - 偏移量(像素)
*/
export function scrollToElement(elementId: string, offset: number = 80): void {
const element = document.getElementById(elementId);
if (element) {
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
}
}
/**
* 检查是否在客户端环境
* @returns 是否在浏览器环境中
*/
export function isClient(): boolean {
return typeof window !== 'undefined';
}
/**
* 获取 URL 查询参数
* @param name - 参数名
* @returns 参数值或 null
*/
export function getQueryParam(name: string): string | null {
if (!isClient()) return null;
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
/**
* 数字格式化函数
* @param num - 数字
* @param locale - 地区设置,默认为 'zh-CN'
* @returns 格式化后的字符串
*/
export function formatNumber(num: number, locale: string = 'zh-CN'): string {
return new Intl.NumberFormat(locale).format(num);
}
/**
* 字节单位转换
* @param bytes - 字节数
* @param decimals - 小数位数,默认为 2
* @returns 格式化后的字符串
*/
export function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* 驼峰命名转短横线命名
* @param str - 驼峰命名字符串
* @returns 短横线命名字符串
*/
export function camelToKebab(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
/**
* 短横线命名转驼峰命名
* @param str - 短横线命名字符串
* @returns 驼峰命名字符串
*/
export function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}
/**
* 首字母大写
* @param str - 输入字符串
* @returns 首字母大写后的字符串
*/
export function capitalizeFirst(str: string): string {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* 移除字符串中的 HTML 标签
* @param html - 包含 HTML 标签的字符串
* @returns 纯文本字符串
*/
export function stripHtml(html: string): string {
if (!html) return '';
return html.replace(/<[^>]*>/g, '');
}
/**
* 生成页面 SEO 元数据
* @param title - 页面标题
* @param description - 页面描述
* @param keywords - 关键词
* @returns 元数据对象数组
*/
export function generateSeoMeta(
title: string,
description: string,
keywords: string[] = []
): Array<{ title: string; name: string; content: string }> {
return [
{ title, name: '', content: title },
{ title: '', name: 'description', content: description },
{ title: '', name: 'keywords', content: keywords.join(', ') },
];
}
/**
* 检查对象是否为空
* @param obj - 要检查的对象
* @returns 是否为空
*/
export function isEmpty(obj: object): boolean {
if (obj === null || obj === undefined) return true;
if (Array.isArray(obj)) return obj.length === 0;
if (typeof obj === 'object') return Object.keys(obj).length === 0;
return false;
}
/**
* 深度合并对象
* @param target - 目标对象
* @param sources - 源对象数组
* @returns 合并后的对象
*/
export function deepMerge<T extends object>(target: T, ...sources: Partial<T>[]): T {
if (!sources.length) return target;
const source = sources.shift();
if (source && typeof source === 'object') {
for (const key in source) {
if (source[key] && typeof source[key] === 'object') {
if (!target[key]) Object.assign(target, { [key]: {} });
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
/**
* 防抖函数
* @param func - 要防抖的函数
* @param wait - 等待时间(毫秒)
* @returns 防抖后的函数
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* 节流函数
* @param func - 要节流的函数
* @param limit - 时间限制(毫秒)
* @returns 节流后的函数
*/
export function throttle<T extends (...args: unknown[]) => unknown>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}

438
src/pages/About.tsx Normal file
View File

@@ -0,0 +1,438 @@
import { motion } from 'framer-motion';
import { useRef } from 'react';
import {
Target,
Heart,
Award,
Users,
Calendar,
TrendingUp,
Building2,
Globe,
} from 'lucide-react';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { COMPANY_INFO } from '../lib/constants';
// 发展历程数据
const milestones = [
{
year: '2010',
title: '公司成立',
description: '诚裕集团在北京成立,开始布局金融服务业务',
icon: Building2,
},
{
year: '2012',
title: '首次战略融资',
description: '完成 A 轮融资,获得知名投资机构认可',
icon: TrendingUp,
},
{
year: '2015',
title: '业务扩展',
description: '成立科技研发子公司,进军科技领域',
icon: Globe,
},
{
year: '2018',
title: '规模扩张',
description: '员工规模突破 200 人,服务客户超过 500 家',
icon: Users,
},
{
year: '2020',
title: '产业投资布局',
description: '成立产业投资基金,全面进入投资领域',
icon: Target,
},
{
year: '2023',
title: '集团化运营',
description: '正式更名为诚裕集团,形成多元化业务体系',
icon: Award,
},
{
year: '2025',
title: '新里程碑',
description: '管理资产突破 500 亿,员工规模超过 500 人',
icon: Calendar,
},
];
// 核心价值观数据
const coreValues = [
{
icon: Heart,
title: '诚信为本',
description: '诚信是企业发展的基石,我们始终坚守诚信底线,与客户、合作伙伴建立长期信任关系',
color: 'from-red-500 to-red-600',
},
{
icon: Target,
title: '创新驱动',
description: '创新是企业发展的动力,我们持续投入研发,不断推出创新产品和服务',
color: 'from-blue-500 to-blue-600',
},
{
icon: Award,
title: '卓越品质',
description: '品质是企业生存的根本,我们追求卓越,确保每一个项目都达到最高标准',
color: 'from-yellow-500 to-yellow-600',
},
{
icon: Users,
title: '共赢合作',
description: '合作是企业成功的关键,我们与客户、员工、合作伙伴实现互利共赢',
color: 'from-green-500 to-green-600',
},
];
// 团队成员数据
const teamMembers = [
{
name: '张明远',
position: '董事长兼 CEO',
bio: '毕业于清华大学金融系,拥有 20 年金融行业经验,曾任多家知名金融机构高管。',
},
{
name: '李晓峰',
position: '首席财务官 CFO',
bio: '持有注册会计师资格,曾在四大会计师事务所工作 15 年,专业财务管理和资本运作专家。',
},
{
name: '王建华',
position: '首席技术官 CTO',
bio: '计算机科学博士,曾在国内外知名科技公司担任技术负责人,拥有多项技术专利。',
},
{
name: '陈静雅',
position: '首席运营官 COO',
bio: 'MBA 学位,拥有丰富的企业运营管理经验,擅长战略规划和流程优化。',
},
];
// 荣誉资质数据
const honors = [
{ name: '国家级高新技术企业认证', year: '2020' },
{ name: '北京市优秀企业', year: '2021' },
{ name: '中国最佳雇主品牌', year: '2022' },
{ name: '金融科技创新奖', year: '2023' },
{ name: '年度优秀企业', year: '2024' },
{ name: 'ESG 最佳实践奖', year: '2025' },
];
/**
* About 组件 - 关于我们页面
*/
export const About: React.FC = () => {
const timelineRef = useRef<HTMLDivElement>(null);
return (
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航 */}
<Header />
{/* 主内容 */}
<main>
{/* 页面标题区域 */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center text-white"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-4">
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto">
2010
</p>
</motion.div>
</div>
</section>
{/* 公司简介 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-6">
</h2>
<div className="space-y-6 text-gray-600 leading-relaxed">
<p>
<strong>{COMPANY_INFO.fullName}</strong>
{COMPANY_INFO.established} {COMPANY_INFO.headquarters}
</p>
<p>
"诚信、创新、共赢"
</p>
<p>
1000
500 亿
</p>
</div>
{/* 核心数据 */}
<div className="mt-8 grid grid-cols-3 gap-6">
<div className="text-center p-4 bg-primary/5 rounded-xl">
<div className="text-2xl font-bold text-primary">15+</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-accent/10 rounded-xl">
<div className="text-2xl font-bold text-accent-dark">500+</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-green-100 rounded-xl">
<div className="text-2xl font-bold text-green-700">1000+</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</motion.div>
<motion.div
className="relative"
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className="aspect-[4/3] rounded-2xl overflow-hidden bg-gradient-to-br from-primary to-primary-light shadow-xl">
<div className="w-full h-full flex items-center justify-center text-white p-12 text-center">
<div>
<div className="text-6xl font-bold mb-4"></div>
<div className="text-2xl opacity-80"> · </div>
<div className="mt-8 w-24 h-1 bg-accent mx-auto rounded-full" />
</div>
</div>
</div>
{/* 装饰元素 */}
<div className="absolute -bottom-6 -right-6 w-32 h-32 bg-accent/20 rounded-2xl -z-10" />
<div className="absolute -top-6 -left-6 w-24 h-24 bg-primary/10 rounded-2xl -z-10" />
</motion.div>
</div>
</div>
</section>
{/* 发展历程时间线 */}
<section className="py-20 bg-white" ref={timelineRef}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
{/* 时间线 */}
<div className="relative">
{/* 中轴线 */}
<div className="absolute left-1/2 -translate-x-1/2 w-0.5 h-full bg-gradient-to-b from-primary via-accent to-primary-light hidden md:block" />
{/* 时间线项目 */}
<div className="space-y-12">
{milestones.map((milestone, index) => (
<motion.div
key={milestone.year}
className={`flex items-center gap-8 ${
index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'
}`}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
{/* 内容区域 */}
<div className="flex-1 text-center md:text-left">
<div
className={`p-6 bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow ${
index % 2 === 0 ? 'md:pr-12' : 'md:pl-12'
}`}
>
<span className="inline-block px-3 py-1 bg-accent/20 text-accent-dark text-sm font-semibold rounded-full mb-3">
{milestone.year}
</span>
<h3 className="text-xl font-semibold text-primary-dark mb-2">
{milestone.title}
</h3>
<p className="text-gray-600">{milestone.description}</p>
</div>
</div>
{/* 中间图标 */}
<div className="hidden md:flex items-center justify-center w-12 h-12 rounded-full bg-primary text-white shadow-lg z-10">
<milestone.icon size={20} />
</div>
{/* 空白区域 */}
<div className="flex-1 hidden md:block" />
</motion.div>
))}
</div>
</div>
</div>
</section>
{/* 企业文化 */}
<section className="py-20 bg-primary-dark text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold mb-4"></h2>
<p className="text-lg text-gray-300">
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{coreValues.map((value, index) => (
<motion.div
key={value.title}
className="text-center p-6 rounded-2xl bg-white/5 hover:bg-white/10 transition-colors"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
<div
className={`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br ${value.color} text-white mb-5`}
>
<value.icon size={32} />
</div>
<h3 className="text-xl font-semibold mb-3">{value.title}</h3>
<p className="text-gray-300 text-sm leading-relaxed">
{value.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* 团队介绍 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{teamMembers.map((member, index) => (
<motion.div
key={member.name}
className="text-center p-6 bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-lg transition-shadow"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
{/* 头像占位 */}
<div className="w-24 h-24 mx-auto mb-4 rounded-full bg-gradient-to-br from-primary to-primary-light flex items-center justify-center text-white text-2xl font-bold">
{member.name.charAt(0)}
</div>
<h3 className="text-xl font-semibold text-primary-dark">
{member.name}
</h3>
<p className="text-accent font-medium mb-3">{member.position}</p>
<p className="text-gray-600 text-sm leading-relaxed">
{member.bio}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* 资质荣誉 */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{honors.map((honor, index) => (
<motion.div
key={honor.name}
className="flex items-center gap-4 p-5 bg-background rounded-xl hover:shadow-md transition-shadow"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ x: 5 }}
>
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-accent/20 flex items-center justify-center">
<Award size={24} className="text-accent" />
</div>
<div className="flex-1">
<h4 className="font-medium text-primary-dark">
{honor.name}
</h4>
<p className="text-sm text-gray-500">{honor.year}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
</main>
{/* 页脚 */}
<Footer />
</motion.div>
);
};
export default About;

557
src/pages/Contact.tsx Normal file
View File

@@ -0,0 +1,557 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import {
MapPin,
Phone,
Mail,
Clock,
Send,
User,
MessageSquare,
Building2,
Briefcase,
ArrowRight,
CheckCircle,
} from 'lucide-react';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { COMPANY_INFO, CONTACT_INFO } from '../lib/constants';
/**
* Contact 组件 - 联系我们页面
*/
// 招聘信息数据
const jobPositions = [
{
id: 1,
title: '高级投资经理',
department: '投资部',
location: '北京',
type: '全职',
salary: '30K-50K/月',
},
{
id: 2,
title: 'Java 开发工程师',
department: '技术部',
location: '北京',
type: '全职',
salary: '25K-40K/月',
},
{
id: 3,
title: '财务顾问',
department: '金融部',
location: '上海',
type: '全职',
salary: '20K-35K/月',
},
{
id: 4,
title: '产品经理',
department: '产品部',
location: '北京',
type: '全职',
salary: '28K-45K/月',
},
];
// 表单状态类型
interface ContactFormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
subject?: string;
message?: string;
}
/**
* Contact 组件
*/
export const Contact: React.FC = () => {
const [formData, setFormData] = useState<ContactFormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
// 表单验证
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name.trim()) {
newErrors.name = '请输入您的姓名';
}
if (!formData.email.trim()) {
newErrors.email = '请输入您的邮箱';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '请输入有效的邮箱地址';
}
if (!formData.subject.trim()) {
newErrors.subject = '请输入邮件主题';
}
if (!formData.message.trim()) {
newErrors.message = '请输入留言内容';
} else if (formData.message.trim().length < 10) {
newErrors.message = '留言内容至少需要 10 个字符';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理输入变化
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// 清除对应字段的错误
if (errors[name as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
};
// 处理表单提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
// 模拟表单提交
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setSubmitSuccess(true);
setFormData({ name: '', email: '', subject: '', message: '' });
// 3秒后重置成功状态
setTimeout(() => setSubmitSuccess(false), 3000);
};
return (
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航 */}
<Header />
{/* 主内容 */}
<main>
{/* 页面标题 */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center text-white"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-4">
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto">
</p>
</motion.div>
</div>
</section>
{/* 联系信息 */}
<section className="py-16 -mt-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{CONTACT_INFO.map((info, index) => {
const Icon = info.icon;
return (
<motion.div
key={info.id}
className="bg-white rounded-xl shadow-lg p-6 text-center hover:shadow-xl transition-shadow"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-accent/20 mb-4">
<Icon size={28} className="text-accent" />
</div>
<h3 className="text-sm text-gray-500 mb-2">{info.title}</h3>
<p className="font-medium text-primary-dark">{info.content}</p>
</motion.div>
);
})}
</div>
</div>
</section>
{/* 联系表单和地图 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12">
{/* 联系表单 */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-primary-dark mb-2">
</h2>
<p className="text-gray-600 mb-8">
</p>
{/* 成功提示 */}
{submitSuccess && (
<motion.div
className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center gap-3"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
>
<CheckCircle size={24} className="text-green-600" />
<div>
<p className="font-medium text-green-800"></p>
<p className="text-sm text-green-600">
</p>
</div>
</motion.div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* 姓名 */}
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<User
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="请输入您的姓名"
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all ${
errors.name
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
</div>
{errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* 邮箱 */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<Mail
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all ${
errors.email
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
</div>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
{/* 主题 */}
<div>
<label
htmlFor="subject"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<MessageSquare
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<select
id="subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all appearance-none bg-white ${
errors.subject
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
>
<option value=""></option>
<option value="业务咨询"></option>
<option value="投资合作"></option>
<option value="技术支持"></option>
<option value="媒体合作"></option>
<option value="其他"></option>
</select>
</div>
{errors.subject && (
<p className="mt-1 text-sm text-red-500">{errors.subject}</p>
)}
</div>
{/* 留言内容 */}
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
placeholder="请详细描述您的需求..."
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all resize-none ${
errors.message
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
{errors.message && (
<p className="mt-1 text-sm text-red-500">{errors.message}</p>
)}
</div>
{/* 提交按钮 */}
<motion.button
type="submit"
disabled={isSubmitting}
className={`w-full py-4 rounded-lg font-medium text-white transition-all flex items-center justify-center gap-2 ${
isSubmitting
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary hover:bg-primary-light'
}`}
whileHover={!isSubmitting ? { scale: 1.02 } : {}}
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
>
{isSubmitting ? (
<>
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
...
</>
) : (
<>
<Send size={20} />
</>
)}
</motion.button>
</form>
</div>
</motion.div>
{/* 地图和地址信息 */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{/* 地图占位 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-2 mb-6">
<div className="aspect-[4/3] rounded-xl bg-gradient-to-br from-primary/10 to-primary-light/10 flex flex-col items-center justify-center">
<MapPin size={48} className="text-primary mb-4" />
<p className="text-primary-dark font-medium mb-2">
</p>
<p className="text-gray-500 text-sm text-center px-8">
88
</p>
<motion.button
className="mt-4 px-6 py-2 bg-primary text-white rounded-lg text-sm"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
</motion.button>
</div>
</div>
{/* 公司详细信息 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h3 className="text-lg font-semibold text-primary-dark mb-4">
</h3>
<div className="space-y-4">
<div className="flex items-start gap-3">
<MapPin size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">
{COMPANY_INFO.headquarters}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Phone size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500">线</p>
<p className="text-primary-dark">{COMPANY_INFO.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Mail size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">{COMPANY_INFO.email}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Clock size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">{COMPANY_INFO.workingHours}</p>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
{/* 招聘信息 */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-6">
{jobPositions.map((job, index) => (
<motion.div
key={job.id}
className="p-6 bg-background rounded-xl border border-gray-100 hover:shadow-md transition-shadow"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -3 }}
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-primary-dark mb-1">
{job.title}
</h3>
<p className="text-sm text-gray-500 flex items-center gap-4">
<span className="flex items-center gap-1">
<Building2 size={14} />
{job.department}
</span>
<span className="flex items-center gap-1">
<MapPin size={14} />
{job.location}
</span>
</p>
</div>
<span className="px-3 py-1 bg-accent/20 text-accent-dark text-sm font-medium rounded-full">
{job.salary}
</span>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<span className="text-sm text-gray-500">{job.type}</span>
<motion.button
className="flex items-center gap-1 text-primary font-medium text-sm hover:text-primary-light"
whileHover={{ x: 3 }}
>
<ArrowRight size={16} />
</motion.button>
</div>
</motion.div>
))}
</div>
<motion.div
className="text-center mt-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<motion.button
className="inline-flex items-center gap-2 px-8 py-3 border-2 border-primary text-primary font-medium rounded-lg hover:bg-primary hover:text-white transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Briefcase size={20} />
</motion.button>
</motion.div>
</div>
</section>
</main>
{/* 页脚 */}
<Footer />
</motion.div>
);
};
export default Contact;

View File

@@ -1,116 +1,45 @@
import React, { useEffect, useState } from 'react'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { PostCard } from '../components/PostCard'
import { PostCardSkeleton } from '../components/PostCardSkeleton'
import { Posts } from '../clientsdk/sdk.gen'
import { createClient } from '../clientsdk/client'
import { customQuerySerializer } from '../clientsdk/querySerializer'
import { TENANT_SLUG, TENANT_API_KEY, API_URL } from '../config'
const client = createClient({
baseUrl: API_URL,
querySerializer: customQuerySerializer,
headers: {
'X-Tenant-Slug': TENANT_SLUG,
'X-API-Key': TENANT_API_KEY,
},
})
import { motion } from 'framer-motion';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { Hero } from '../components/Hero';
import { AboutSection } from '../components/Home/AboutSection';
import { ServicesSection } from '../components/Home/ServicesSection';
import { NewsSection } from '../components/Home/NewsSection';
/**
* Home 组件 - 企业官网首页
*/
export const Home: React.FC = () => {
const [posts, setPosts] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true)
setError(null)
const response = await Posts.listPosts({
client,
query: {
limit: 10,
sort: '-createdAt',
},
})
setPosts((response as any)?.data?.docs || [])
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
console.error('获取文章失败:', err)
} finally {
setLoading(false)
}
}
fetchPosts()
}, [])
const stripHtml = (html: string): string => {
const tmp = document.createElement('div')
tmp.innerHTML = html
return tmp.textContent || tmp.innerText || ''
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
const getCategoryTitle = (post: any): string | undefined => {
// categories is an array, get the first one
return post.categories?.[0]?.title
}
const handlePostClick = (slug: string) => {
window.location.href = `/posts/${slug}`
}
return (
<div className="min-h-screen bg-gray-50">
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航栏 */}
<Header />
<main className="container mx-auto px-4 py-8">
<section className="mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-2">📚 </h2>
<p className="text-gray-600"></p>
</section>
{/* 主内容区域 */}
<main>
{/* Hero 区域 - 首页大图 */}
<Hero />
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
<strong></strong> {error}
</div>
)}
{/* 关于我们区域 */}
<AboutSection />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{loading
? Array.from({ length: 6 }).map((_, i) => <PostCardSkeleton key={i} />)
: posts.map((post) => (
<PostCard
key={post.id}
title={post.title}
excerpt={stripHtml(post.content_html || post.content?.root?.children?.[0]?.children?.[0]?.text || post.title)}
category={getCategoryTitle(post)}
date={formatDate(post.createdAt)}
onClick={() => handlePostClick(post.slug)}
/>
))}
</div>
{/* 核心业务区域 */}
<ServicesSection />
{!loading && posts.length === 0 && !error && (
<div className="text-center py-12">
<p className="text-gray-500 text-lg"></p>
</div>
)}
{/* 新闻资讯区域 */}
<NewsSection />
</main>
{/* 页脚 */}
<Footer />
</div>
)
}
</motion.div>
);
};
export default Home;

524
src/pages/News.tsx Normal file
View File

@@ -0,0 +1,524 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Calendar,
Eye,
ArrowRight,
Clock,
TrendingUp,
Tag,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { formatDate } from '../lib/utils';
import { NEWS_CATEGORIES } from '../lib/constants';
// 模拟新闻数据
const allNews = [
{
id: 1,
title: '诚裕集团荣获"2025年度优秀企业"称号',
excerpt: '在近日举办的年度企业评选活动中,诚裕集团凭借其卓越的经营业绩和社会责任表现,荣获"2025年度优秀企业"称号。这一荣誉是对诚裕集团多年来坚持创新发展的肯定。',
category: 'company',
date: '2025-12-20',
readCount: 1256,
image: null,
isHot: true,
},
{
id: 2,
title: '金融科技创新论坛圆满落幕,诚裕集团分享行业洞察',
excerpt: '诚裕集团受邀参加金融科技创新论坛,与行业专家共同探讨金融科技发展趋势,分享公司在数字化转型方面的实践经验。与会嘉宾对诚裕集团的创新成果给予了高度评价。',
category: 'industry',
date: '2025-12-15',
readCount: 892,
image: null,
isHot: true,
},
{
id: 3,
title: '诚裕集团完成新一轮战略融资,估值突破百亿',
excerpt: '诚裕集团宣布完成新一轮战略融资,本轮融资由知名投资机构领投,估值突破百亿元人民币,标志着公司发展进入新阶段。此轮融资将用于加大技术研发和市场拓展力度。',
category: 'company',
date: '2025-12-10',
readCount: 2341,
image: null,
isHot: true,
},
{
id: 4,
title: '诚裕集团发布2025年度社会责任报告',
excerpt: '诚裕集团正式发布《2025年度社会责任报告》全面展示了公司在环境保护、社会公益、公司治理等方面的实践成果。报告显示诚裕集团在ESG领域取得了显著进步。',
category: 'company',
date: '2025-12-05',
readCount: 567,
image: null,
isHot: false,
},
{
id: 5,
title: '人工智能赋能金融服务行业论坛成功举办',
excerpt: '由诚裕集团主办的人工智能赋能金融服务行业论坛在北京成功举办。来自金融机构、科技公司、学术机构的专家学者共同探讨AI技术在金融服务领域的应用前景。',
category: 'industry',
date: '2025-11-28',
readCount: 743,
image: null,
isHot: false,
},
{
id: 6,
title: '诚裕集团获得国家级高新技术企业认证',
excerpt: '诚裕集团正式获得国家级高新技术企业认证,这标志着集团在技术创新和研发投入方面获得了国家层面的认可。诚裕集团将继续加大研发投入,提升自主创新能力。',
category: 'achievement',
date: '2025-11-20',
readCount: 1089,
image: null,
isHot: true,
},
{
id: 7,
title: '诚裕集团与清华大学签署战略合作协议',
excerpt: '诚裕集团与清华大学正式签署战略合作协议,双方将在人才培养、技术研发、成果转化等方面开展深度合作。这一合作将为诚裕集团的创新发展提供强大的智力支持。',
category: 'company',
date: '2025-11-15',
readCount: 621,
image: null,
isHot: false,
},
{
id: 8,
title: '数字化转型趋势报告发布,诚裕集团引领行业发展',
excerpt: '诚裕集团研究院发布《2025企业数字化转型趋势报告》深入分析了当前数字化转型的发展态势和未来趋势。报告指出数字化转型已成为企业提升竞争力的关键路径。',
category: 'industry',
date: '2025-11-08',
readCount: 456,
image: null,
isHot: false,
},
{
id: 9,
title: '诚裕集团获评"最佳雇主品牌"荣誉称号',
excerpt: '在2025年度人力资源管理峰会上诚裕集团凭借其在人才培养、员工发展、企业文化等方面的卓越表现荣获"最佳雇主品牌"荣誉称号。这一荣誉体现了员工对诚裕集团的高度认可。',
category: 'achievement',
date: '2025-11-01',
readCount: 389,
image: null,
isHot: false,
},
{
id: 10,
title: '诚裕集团启动"绿色金融"专项计划',
excerpt: '诚裕集团正式宣布启动"绿色金融"专项计划计划在未来三年内投入100亿元支持绿色产业发展。这一计划的推出体现了诚裕集团积极践行可持续发展理念的决心。',
category: 'company',
date: '2025-10-25',
readCount: 723,
image: null,
isHot: false,
},
{
id: 11,
title: '产业投资基金发展趋势研讨会在京举行',
excerpt: '由诚裕集团主办的产业投资基金发展趋势研讨会在北京举行。来自监管部门、投资机构、产业龙头企业的代表就产业投资基金的发展方向和投资策略进行了深入探讨。',
category: 'industry',
date: '2025-10-18',
readCount: 312,
image: null,
isHot: false,
},
{
id: 12,
title: '诚裕集团入选"北京民营企业百强"榜单',
excerpt: '北京市工商联发布2025年度北京民营企业百强榜单诚裕集团凭借优异的经营业绩和创新能力成功入选该榜单。这一荣誉是对诚裕集团综合实力的又一次肯定。',
category: 'achievement',
date: '2025-10-10',
readCount: 567,
image: null,
isHot: false,
},
];
// 新闻分类映射
const categoryMap: Record<string, { label: string; color: string }> = {
all: { label: '全部', color: 'bg-primary text-white' },
company: { label: '公司动态', color: 'bg-primary/10 text-primary' },
industry: { label: '行业资讯', color: 'bg-accent/20 text-accent-dark' },
achievement: { label: '荣誉资质', color: 'bg-green-100 text-green-700' },
};
// 每页新闻数量
const ITEMS_PER_PAGE = 6;
/**
* 新闻卡片组件
*/
const NewsCard: React.FC<{ news: typeof allNews[0]; index: number }> = ({
news,
index,
}) => {
const category = categoryMap[news.category] || categoryMap.company;
return (
<motion.article
className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-lg transition-all duration-300 group"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ delay: index * 0.05, duration: 0.4 }}
whileHover={{ y: -5 }}
>
{/* 图片区域 */}
<Link to={`/news/${news.id}`} className="block aspect-[16/9] bg-gradient-to-br from-primary/5 to-primary-light/10 relative overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center text-primary/20">
<Tag size={48} />
</div>
{/* 分类标签 */}
<span className={`absolute top-4 left-4 px-3 py-1 text-xs font-medium rounded-full ${category.color}`}>
{category.label}
</span>
{/* 热门标签 */}
{news.isHot && (
<span className="absolute top-4 right-4 px-3 py-1 text-xs font-medium bg-red-500 text-white rounded-full flex items-center gap-1">
<TrendingUp size={12} />
</span>
)}
</Link>
{/* 内容区域 */}
<div className="p-6">
{/* 元信息 */}
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<div className="flex items-center gap-1">
<Calendar size={14} />
<time dateTime={news.date}>{formatDate(news.date, 'YYYY年MM月DD日')}</time>
</div>
<div className="flex items-center gap-1">
<Eye size={14} />
<span>{news.readCount}</span>
</div>
</div>
{/* 标题 */}
<Link to={`/news/${news.id}`}>
<h3 className="text-lg font-semibold text-primary-dark mb-3 line-clamp-2 group-hover:text-primary transition-colors">
{news.title}
</h3>
</Link>
{/* 摘要 */}
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2 mb-4">
{news.excerpt}
</p>
{/* 阅读更多 */}
<Link
to={`/news/${news.id}`}
className="inline-flex items-center gap-2 text-sm font-medium text-primary group-hover:text-primary-light transition-colors"
>
<motion.span
initial={{ x: 0 }}
whileHover={{ x: 5 }}
transition={{ duration: 0.2 }}
>
<ArrowRight size={16} />
</motion.span>
</Link>
</div>
</motion.article>
);
};
/**
* 侧边栏热门新闻组件
*/
const HotNewsWidget: React.FC<{ news: typeof allNews[0]; rank: number }> = ({
news,
rank,
}) => (
<Link
to={`/news/${news.id}`}
className="flex items-start gap-4 p-4 bg-background rounded-xl hover:bg-white hover:shadow-sm transition-all group"
>
<span className="flex-shrink-0 w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
{rank}
</span>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-primary-dark mb-1 line-clamp-2 group-hover:text-primary transition-colors">
{news.title}
</h4>
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{formatDate(news.date, 'YYYY-MM-DD')}</span>
<span>·</span>
<span>{news.readCount} </span>
</div>
</div>
</Link>
);
/**
* 分页组件
*/
const Pagination: React.FC<{
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}> = ({ currentPage, totalPages, onPageChange }) => {
return (
<div className="flex items-center justify-center gap-2 mt-12">
{/* 上一页按钮 */}
<motion.button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
{/* 页码 */}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<motion.button
key={page}
onClick={() => onPageChange(page)}
className={`w-10 h-10 text-sm font-medium rounded-lg transition-colors ${
currentPage === page
? 'bg-primary text-white'
: 'text-gray-600 bg-white border border-gray-200 hover:bg-gray-50'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{page}
</motion.button>
))}
{/* 下一页按钮 */}
<motion.button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
);
};
/**
* News 组件 - 新闻资讯页面
*/
export const News: React.FC = () => {
const [activeCategory, setActiveCategory] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
// 根据分类筛选新闻
const filteredNews =
activeCategory === 'all'
? allNews
: allNews.filter((news) => news.category === activeCategory);
// 计算分页
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const currentNews = filteredNews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
// 获取热门新闻 TOP 5
const hotNews = allNews
.filter((news) => news.isHot)
.sort((a, b) => b.readCount - a.readCount)
.slice(0, 5);
// 切换分类时重置分页
const handleCategoryChange = (category: string) => {
setActiveCategory(category);
setCurrentPage(1);
};
return (
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航 */}
<Header />
{/* 主内容 */}
<main>
{/* 页面标题 */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center text-white"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-4">
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto">
</p>
</motion.div>
</div>
</section>
{/* 新闻内容区域 */}
<section className="py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-3 gap-8">
{/* 左侧新闻列表 */}
<div className="lg:col-span-2">
{/* 分类筛选 */}
<motion.div
className="flex flex-wrap gap-2 mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{NEWS_CATEGORIES.map((category) => (
<motion.button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={`px-5 py-2 text-sm font-medium rounded-lg transition-all ${
activeCategory === category.id
? 'bg-primary text-white shadow-lg shadow-primary/25'
: 'bg-white text-gray-600 hover:bg-gray-50 border border-gray-200'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{category.label}
</motion.button>
))}
</motion.div>
{/* 新闻数量提示 */}
<motion.p
className="text-sm text-gray-500 mb-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<span className="font-medium text-primary">{filteredNews.length}</span>
</motion.p>
{/* 新闻列表 */}
<AnimatePresence mode="wait">
<motion.div
key={`${activeCategory}-${currentPage}`}
className="grid sm:grid-cols-2 gap-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{currentNews.map((news, index) => (
<NewsCard key={news.id} news={news} index={index} />
))}
</motion.div>
</AnimatePresence>
{/* 空状态 */}
{currentNews.length === 0 && (
<motion.div
className="text-center py-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 mb-4">
<Clock size={32} className="text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-600 mb-2">
</h3>
<p className="text-gray-400">
</p>
</motion.div>
)}
{/* 分页 */}
{totalPages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
)}
</div>
{/* 右侧侧边栏 */}
<div className="lg:col-span-1">
<div className="sticky top-24 space-y-8">
{/* 热门新闻 */}
<motion.div
className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-red-100 rounded-lg">
<TrendingUp size={20} className="text-red-500" />
</div>
<h3 className="text-lg font-semibold text-primary-dark">
</h3>
</div>
<div className="space-y-3">
{hotNews.map((news, index) => (
<HotNewsWidget key={news.id} news={news} rank={index + 1} />
))}
</div>
</motion.div>
{/* 关注我们 */}
<motion.div
className="bg-gradient-to-br from-primary to-primary-light rounded-2xl p-6 text-white"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<h3 className="text-lg font-semibold mb-4"></h3>
<p className="text-white/80 text-sm mb-4">
</p>
<div className="flex gap-3">
<motion.button
className="flex-1 py-2 bg-white/20 rounded-lg text-sm font-medium hover:bg-white/30 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
<motion.button
className="flex-1 py-2 bg-white/20 rounded-lg text-sm font-medium hover:bg-white/30 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</motion.div>
</div>
</div>
</div>
</div>
</section>
</main>
{/* 页脚 */}
<Footer />
</motion.div>
);
};
export default News;

549
src/pages/Services.tsx Normal file
View File

@@ -0,0 +1,549 @@
import { motion } from 'framer-motion';
import { useState } from 'react';
import {
TrendingUp,
Cpu,
Building2,
Briefcase,
CheckCircle,
Target,
BarChart3,
Shield,
Lightbulb,
Rocket,
Handshake,
PieChart,
Globe,
Users,
Zap,
} from 'lucide-react';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { SERVICES } from '../lib/constants';
/**
* Services 组件 - 产品服务页面
*/
// 详细服务数据
const serviceDetails = {
finance: {
description: '我们的金融服务涵盖资产配置、投资顾问、风险管理等全方位金融解决方案,帮助客户实现财富稳健增长。',
features: [
{ icon: PieChart, title: '资产配置', desc: '根据客户风险偏好和投资目标,提供个性化的资产配置方案' },
{ icon: TrendingUp, title: '投资顾问', desc: '专业投资顾问团队,提供一对一的投资咨询和指导服务' },
{ icon: Shield, title: '风险管理', desc: '完善的风险控制体系,确保投资安全可控' },
{ icon: BarChart3, title: '业绩报告', desc: '定期提供详细的业绩报告和分析,让客户清晰了解投资状况' },
],
process: [
{ step: '01', title: '需求沟通', desc: '了解客户投资目标和风险偏好' },
{ step: '02', title: '方案设计', desc: '制定个性化资产配置方案' },
{ step: '03', title: '投资执行', desc: '按照方案进行投资操作' },
{ step: '04', title: '持续跟踪', desc: '定期调整和优化投资组合' },
],
},
tech: {
description: '我们聚焦人工智能、大数据、云计算等前沿技术,为企业提供全方位的数字化转型解决方案。',
features: [
{ icon: Cpu, title: 'AI 解决方案', desc: '利用人工智能技术,帮助企业实现智能化升级' },
{ icon: Lightbulb, title: '大数据分析', desc: '深度挖掘数据价值,提供决策支持' },
{ icon: Zap, title: '云服务', desc: '为企业提供稳定、高效的云计算服务' },
{ icon: Rocket, title: '产品研发', desc: '定制化软件产品开发和创新解决方案' },
],
process: [
{ step: '01', title: '需求分析', desc: '深入了解企业数字化需求' },
{ step: '02', title: '方案规划', desc: '制定数字化转型路线图' },
{ step: '03', title: '开发实施', desc: '按照方案进行系统开发和部署' },
{ step: '04', title: '运维支持', desc: '提供持续的技术支持和维护服务' },
],
},
investment: {
description: '我们专注于新兴产业投资,通过战略投资推动产业升级,为投资者创造丰厚回报。',
features: [
{ icon: Target, title: '股权投资', desc: '聚焦高成长性企业,把握产业投资机会' },
{ icon: Building2, title: '产业基金', desc: '设立专项产业基金,支持重点产业发展' },
{ icon: Handshake, title: '并购重组', desc: '协助企业进行并购整合,实现规模扩张' },
{ icon: Globe, title: '跨境投资', desc: '布局全球市场,寻求国际投资机会' },
],
process: [
{ step: '01', title: '项目筛选', desc: '严格筛选优质投资项目' },
{ step: '02', title: '尽职调查', desc: '深入调研项目基本面' },
{ step: '03', title: '投资决策', desc: '专业评审委员会决策' },
{ step: '04', title: '投后管理', desc: '提供增值服务,助力企业成长' },
],
},
consulting: {
description: '我们为企业提供战略规划、运营优化、风险管理等专业咨询服务,助力企业持续发展。',
features: [
{ icon: Target, title: '战略规划', desc: '帮助企业制定长期发展战略' },
{ icon: Users, title: '管理咨询', desc: '优化组织架构和业务流程' },
{ icon: BarChart3, title: '财务咨询', desc: '提供财务规划和优化建议' },
{ icon: Shield, title: '风险咨询', desc: '识别和控制企业经营风险' },
],
process: [
{ step: '01', title: '现状诊断', desc: '全面评估企业现状' },
{ step: '02', title: '问题分析', desc: '深入分析核心问题' },
{ step: '03', title: '方案设计', desc: '制定针对性解决方案' },
{ step: '04', title: '落地实施', desc: '协助方案落地和效果跟踪' },
],
},
};
// 成功案例数据
const cases = [
{
id: 1,
title: '某大型制造企业数字化转型',
category: 'tech',
description: '帮助某大型制造企业完成全面的数字化转型,实现生产效率提升 30%,成本降低 20%。',
result: ['生产效率提升 30%', '成本降低 20%', '库存周转率提升 40%'],
image: null,
},
{
id: 2,
title: '新兴产业基金设立与运营',
category: 'investment',
description: '为某地方政府设立 50 亿元新兴产业投资基金,成功投资 20 余个优质项目。',
result: ['基金规模 50 亿元', '投资项目 20+', '平均回报率 25%'],
image: null,
},
{
id: 3,
title: '企业战略重组咨询项目',
category: 'consulting',
description: '为某上市公司提供战略重组咨询服务,成功完成业务整合,实现市值增长 50%。',
result: ['市值增长 50%', '完成 3 家公司整合', '营收增长 35%'],
image: null,
},
{
id: 4,
title: '高净值客户财富管理',
category: 'finance',
description: '为某高净值客户提供全方位的财富管理服务5 年累计收益达到 120%。',
result: ['累计收益 120%', '资产规模增长 80%', '零重大风险事件'],
image: null,
},
];
// 合作伙伴数据
const partners = [
{ name: '中国银行', industry: '金融' },
{ name: '华为技术', industry: '科技' },
{ name: '中信证券', industry: '金融' },
{ name: '阿里巴巴', industry: '科技' },
{ name: '招商银行', industry: '金融' },
{ name: '腾讯云', industry: '科技' },
{ name: '建设银行', industry: '金融' },
{ name: '字节跳动', industry: '科技' },
];
// 分类标签
const categories = [
{ id: 'all', label: '全部' },
{ id: 'finance', label: '金融服务' },
{ id: 'tech', label: '科技研发' },
{ id: 'investment', label: '产业投资' },
{ id: 'consulting', label: '咨询服务' },
];
/**
* 成功案例卡片组件
*/
const CaseCard: React.FC<{ case: typeof cases[0]; index: number }> = ({ case: caseItem, index }) => (
<motion.div
className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-lg transition-shadow"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
{/* 图片占位 */}
<div className="aspect-[16/9] bg-gradient-to-br from-primary/5 to-primary-light/10 flex items-center justify-center">
<div className="text-primary/20">
<Rocket size={48} />
</div>
</div>
<div className="p-6">
<span className="inline-block px-3 py-1 text-xs font-medium bg-accent/20 text-accent-dark rounded-full mb-3">
{categories.find(c => c.id === caseItem.category)?.label}
</span>
<h3 className="text-xl font-semibold text-primary-dark mb-3">
{caseItem.title}
</h3>
<p className="text-gray-600 text-sm mb-4">{caseItem.description}</p>
<div className="space-y-2">
{caseItem.result.map((result, i) => (
<div key={i} className="flex items-center gap-2 text-sm text-gray-600">
<CheckCircle size={14} className="text-accent flex-shrink-0" />
{result}
</div>
))}
</div>
</div>
</motion.div>
);
/**
* Services 组件
*/
export const Services: React.FC = () => {
const [activeCategory, setActiveCategory] = useState('all');
const filteredCases = activeCategory === 'all'
? cases
: cases.filter(c => c.category === activeCategory);
return (
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航 */}
<Header />
{/* 主内容 */}
<main>
{/* 页面标题 */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center text-white"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-4">
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto">
</p>
</motion.div>
</div>
</section>
{/* 服务概览 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
{/* 服务卡片 */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{SERVICES.map((service, index) => {
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
TrendingUp,
Cpu,
Building2,
Briefcase,
};
const Icon = iconMap[service.icon] || TrendingUp;
return (
<motion.div
key={service.id}
className="group p-8 bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-lg transition-all duration-300"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -8 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-primary-light text-white mb-6 group-hover:scale-110 transition-transform duration-300">
<Icon size={32} />
</div>
<h3 className="text-xl font-semibold text-primary-dark mb-4">
{service.title}
</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-6">
{service.description}
</p>
<ul className="space-y-2">
{service.features.map((feature, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
<CheckCircle size={14} className="text-accent" />
{feature}
</li>
))}
</ul>
</motion.div>
);
})}
</div>
</div>
</section>
{/* 详细服务介绍 - 使用 Tab 切换 */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
{/* 使用静态展示代替 Tab - 展示金融服务 */}
<div className="grid lg:grid-cols-2 gap-12 items-center mb-20">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="inline-block px-4 py-1 bg-accent/20 text-accent-dark text-sm font-semibold rounded-full mb-4">
</span>
<h3 className="text-2xl font-bold text-primary-dark mb-6">
</h3>
<p className="text-gray-600 leading-relaxed mb-8">
{serviceDetails.finance.description}
</p>
{/* 特性列表 */}
<div className="grid sm:grid-cols-2 gap-4 mb-8">
{serviceDetails.finance.features.map((feature, index) => (
<div key={index} className="flex items-start gap-3 p-4 bg-background rounded-xl">
<div className="p-2 bg-primary/10 rounded-lg">
<feature.icon size={20} className="text-primary" />
</div>
<div>
<h4 className="font-medium text-primary-dark text-sm">
{feature.title}
</h4>
<p className="text-xs text-gray-500 mt-1">{feature.desc}</p>
</div>
</div>
))}
</div>
</motion.div>
<motion.div
className="relative"
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="aspect-square rounded-2xl bg-gradient-to-br from-primary to-primary-light p-8 flex items-center justify-center">
<div className="text-center text-white">
<PieChart size={80} className="mx-auto mb-6 opacity-80" />
<div className="text-4xl font-bold mb-2">500亿</div>
<div className="text-lg opacity-80"></div>
</div>
</div>
</motion.div>
</div>
{/* 科技研发 */}
<div className="grid lg:grid-cols-2 gap-12 items-center mb-20">
<motion.div
className="order-2 lg:order-1"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="aspect-square rounded-2xl bg-gradient-to-br from-accent to-accent-dark p-8 flex items-center justify-center">
<div className="text-center text-primary-dark">
<Cpu size={80} className="mx-auto mb-6 opacity-80" />
<div className="text-4xl font-bold mb-2">100+</div>
<div className="text-lg opacity-80"></div>
</div>
</div>
</motion.div>
<motion.div
className="order-1 lg:order-2"
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="inline-block px-4 py-1 bg-primary/10 text-primary text-sm font-semibold rounded-full mb-4">
</span>
<h3 className="text-2xl font-bold text-primary-dark mb-6">
</h3>
<p className="text-gray-600 leading-relaxed mb-8">
{serviceDetails.tech.description}
</p>
<div className="grid sm:grid-cols-2 gap-4 mb-8">
{serviceDetails.tech.features.map((feature, index) => (
<div key={index} className="flex items-start gap-3 p-4 bg-background rounded-xl">
<div className="p-2 bg-accent/20 rounded-lg">
<feature.icon size={20} className="text-accent-dark" />
</div>
<div>
<h4 className="font-medium text-primary-dark text-sm">
{feature.title}
</h4>
<p className="text-xs text-gray-500 mt-1">{feature.desc}</p>
</div>
</div>
))}
</div>
</motion.div>
</div>
</div>
</section>
{/* 成功案例 */}
<section className="py-20 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="flex flex-col md:flex-row md:items-end md:justify-between gap-4 mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div>
<h2 className="text-3xl font-bold text-primary-dark mb-2">
</h2>
<p className="text-lg text-gray-600">
</p>
</div>
{/* 筛选标签 */}
<div className="flex flex-wrap gap-2">
{categories.slice(0, 5).map((category) => (
<button
key={category.id}
onClick={() => setActiveCategory(category.id)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all ${
activeCategory === category.id
? 'bg-primary text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
{category.label}
</button>
))}
</div>
</motion.div>
{/* 案例网格 */}
<div className="grid md:grid-cols-2 lg:grid-cols-2 gap-8">
{filteredCases.map((caseItem, index) => (
<CaseCard key={caseItem.id} case={caseItem} index={index} />
))}
</div>
</div>
</section>
{/* 合作伙伴 */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{partners.map((partner, index) => (
<motion.div
key={partner.name}
className="p-6 bg-background rounded-xl hover:shadow-md transition-shadow flex flex-col items-center"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -3 }}
>
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-primary/10 to-primary-light/10 flex items-center justify-center mb-4">
<Building2 size={32} className="text-primary" />
</div>
<h4 className="font-semibold text-primary-dark">
{partner.name}
</h4>
<p className="text-sm text-gray-500">{partner.industry}</p>
</motion.div>
))}
</div>
</div>
</section>
{/* 服务流程 */}
<section className="py-20 bg-primary-dark text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold mb-4"></h2>
<p className="text-lg text-gray-300">
</p>
</motion.div>
<div className="grid md:grid-cols-4 gap-8">
{serviceDetails.finance.process.map((step, index) => (
<motion.div
key={step.step}
className="text-center"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-accent text-primary-dark font-bold text-xl mb-6">
{step.step}
</div>
<h3 className="text-xl font-semibold mb-3">{step.title}</h3>
<p className="text-gray-400 text-sm">{step.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
</main>
{/* 页脚 */}
<Footer />
</motion.div>
);
};
export default Services;

View File

@@ -5,7 +5,30 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
extend: {
colors: {
// 企业主题色
primary: {
DEFAULT: '#1e3a8a', // 深蓝色 - 主色调
light: '#2563eb', // 浅蓝色 - hover 状态
dark: '#1e293b', // 深色 - 文字
},
accent: {
DEFAULT: '#d4af37', // 金色 - 辅助色
light: '#e5c158', // 浅金色 - hover
dark: '#b8960c', // 深金色
},
background: {
DEFAULT: '#f8fafc', // 浅灰色 - 背景色
light: '#ffffff', // 白色
dark: '#f1f5f9', // 深浅灰
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [
require('@tailwindcss/typography'),