refactor: 页面组件优化与墨韵分割线组件化

- 提取 AnimatedInkDivider 组件替代硬编码 ink-divider div
- 重构各营销页面组件代码结构优化
- 修正统计数据:自研产品 4 -> 6
- 更新 about 页面测试用例
This commit is contained in:
张翔
2026-04-29 19:15:58 +08:00
parent ec3e89f591
commit 6ae0f1274f
17 changed files with 914 additions and 1010 deletions
+130 -236
View File
@@ -1,253 +1,147 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef, useMemo } from 'react';
import { COMPANY_INFO, STATS } from '@/lib/constants';
import { Card, CardContent } from '@/components/ui/card';
import { PageHeader } from '@/components/ui/page-header';
import { Button } from '@/components/ui/button';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Lightbulb, Users, Target, MapPin, Mail, ArrowRight } from 'lucide-react';
import { RippleButton } from '@/components/ui/ripple-button';
import { COMPANY_INFO } from '@/lib/constants';
import { ArrowRight, Target, HeartHandshake, Award, Shield, Building2, Users, Code, TrendingUp } from 'lucide-react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { InkReveal, BlurReveal, StaggerContainer, StaggerItem, CountUp } from '@/lib/animations';
import { TextReveal } from '@/components/ui/scroll-animations';
const VALUES = [
{ title: '务实', description: '不追逐风口,只做真正为客户创造价值的事。', icon: Target },
{ title: '陪伴', description: '交付只是开始,长期陪跑才是我们的承诺。', icon: HeartHandshake },
{ title: '专业', description: '用扎实的工程能力和行业经验赢得信任。', icon: Award },
];
const TEAM_PILLARS = [
{ icon: Shield, title: '12+ 年行业深耕', description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业。' },
{ icon: Building2, title: '大型 IT 企业背景', description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力和规范化的交付经验。' },
{ icon: Users, title: '复合型技术团队', description: '团队成员既懂技术又懂业务,能够深入理解客户的真实场景和痛点。' },
{ icon: Code, title: '全栈技术能力', description: '掌握从前端到后端、从云原生到数据智能的全栈技术能力。' },
{ icon: TrendingUp, title: '结果导向交付', description: '以"客户业务是否真正改善"为衡量标准,追求可量化的业务价值。' },
];
const COMPANY_STATS = [
{ value: 12, label: '年+核心成员行业经验', suffix: '' },
{ value: 5, label: '覆盖行业', suffix: '+' },
{ value: 6, label: '自研产品', suffix: '款' },
{ value: 98, label: '客户满意度', suffix: '%' },
];
export function AboutClient() {
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const values = useMemo(() => [
{
icon: Lightbulb,
title: '务实',
description: '不追逐风口,只做真正为客户创造价值的事。每一个方案都源于对业务场景的深入洞察。',
},
{
icon: Users,
title: '陪伴',
description: '交付只是开始,长期陪跑才是我们的承诺。我们关注的不只是项目是否上线,更是您的业务是否真正改善。',
},
{
icon: Target,
title: '专业',
description: '用扎实的工程能力和行业经验赢得信任。既懂技术又懂业务,提供真正可落地的解决方案。',
},
], []);
const milestones = useMemo(() => [
{
date: '2026年1月',
title: '公司成立',
description: '四川睿新致远科技有限公司在成都龙泉驿区正式成立,专注于企业数字化转型解决方案',
},
{
date: '2026年1月',
title: '团队组建',
description: '核心团队到位,技术团队拥有丰富的行业经验和专业技能',
},
{
date: '2026年2月',
title: '业务启动',
description: '推出企业数字化转型解决方案,开始服务首批客户',
},
{
date: '2026年2月',
title: '产品研发',
description: '启动ERP、CRM等自研产品的研发工作,致力于为企业提供一站式数字化服务',
},
], []);
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const shouldReduceMotion = useReducedMotion();
return (
<div className="min-h-screen bg-white">
<PageHeader
title="关于我们"
description="了解睿新致远的品牌故事。我们不只是技术供应商,更是您数字化转型的成长伙伴。以智慧连接数字趋势,以伙伴身份陪您成长。"
/>
{/* Hero - InkReveal */}
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<InkReveal className="max-w-4xl">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-px bg-linear-to-r from-[#1C1C1C] to-[var(--color-brand-primary)]" />
<span className="text-sm text-[#5C5C5C] tracking-wide"></span>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="tracking-tight text-[var(--color-brand-primary)]" style={{ fontFamily: "var(--font-aoyagi-reisho), 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif", fontWeight: 'normal', WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale', textRendering: 'optimizeLegibility' }}>{COMPANY_INFO.shortName}</span>
</h1>
<p className="text-lg text-[#5C5C5C] max-w-2xl">{COMPANY_INFO.slogan}</p>
</InkReveal>
</div>
</div>
<div ref={contentRef} className="container-wide py-12 md:py-16">
<div ref={ref} className="container-wide py-12 md:py-16">
{/* 品牌理念 - TextReveal 逐词揭示 */}
<TextReveal
text="企业需要的,不是一个高高在上的专家,也不是一个做完就跑的卖家,而是一个能坐下来、一起想办法的同行者。"
className="text-center text-lg text-[#5C5C5C] leading-relaxed mb-8 max-w-3xl mx-auto"
delay={0.1}
/>
{/* 核心理念 - BlurReveal */}
<BlurReveal delay={0.2} className="bg-[#FFFBF5] rounded-2xl p-8 mb-12 border border-[var(--color-brand-primary)]/20">
<p className="text-[#1C1C1C] font-medium text-center text-lg">
</p>
</BlurReveal>
{/* 数据指标 - CountUp */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="max-w-4xl mx-auto space-y-8"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto mb-16"
>
<div className="bg-[#FFFBF5] rounded-2xl p-8 border border-[#E5E5E5]">
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6"></h2>
<p className="text-[#5C5C5C] mb-6 leading-relaxed">
</p>
<div className="mb-6">
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3"></h3>
<p className="text-[#5C5C5C] mb-3 leading-relaxed">
</p>
<p className="text-[#5C5C5C] mb-3 leading-relaxed">
</p>
<p className="text-[#5C5C5C] leading-relaxed">
</p>
</div>
<div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3"></h3>
<p className="text-[#5C5C5C] mb-3 leading-relaxed">
&ldquo;&rdquo;
</p>
<p className="text-[#5C5C5C] mb-3 leading-relaxed">
</p>
<p className="text-[#5C5C5C] leading-relaxed">
</p>
<p className="text-[#5C5C5C] mt-3 leading-relaxed">
&ldquo;&rdquo;
</p>
</div>
</div>
<div className="bg-[#FFFBF5] rounded-2xl p-8 border border-[#E5E5E5]">
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6"></h2>
<p className="text-[#5C5C5C] mb-6 leading-relaxed">
</p>
<ul className="space-y-3 mb-6">
<li className="flex items-start gap-3">
<span className="text-green-600 font-bold"></span>
<span className="text-[#5C5C5C]"></span>
</li>
<li className="flex items-start gap-3">
<span className="text-green-600 font-bold"></span>
<span className="text-[#5C5C5C]"></span>
</li>
<li className="flex items-start gap-3">
<span className="text-green-600 font-bold"></span>
<span className="text-[#5C5C5C]">&ldquo;&rdquo;</span>
</li>
</ul>
<p className="text-[#5C5C5C] leading-relaxed font-medium">
</p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="grid grid-cols-2 md:grid-cols-4 gap-6"
>
{STATS.map((stat, idx) => (
<Card key={idx} className="text-center border-[#E5E5E5]">
<CardContent className="pt-6">
<div className="text-3xl sm:text-4xl font-bold text-[var(--color-brand-primary)] mb-2">{stat.value}</div>
<div className="text-sm text-[#5C5C5C]">{stat.label}</div>
</CardContent>
</Card>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6 text-center"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{values.map((value, idx) => (
<motion.div
key={value.title}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.4 + idx * 0.1 }}
className="flex items-start gap-4 p-6 bg-[#FFFBF5] rounded-xl border border-[#E5E5E5] hover:border-[#1C1C1C] transition-all duration-300"
>
<div className="w-12 h-12 rounded-lg bg-[var(--color-brand-primary)] flex items-center justify-center shrink-0">
<value.icon className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2 text-[#1C1C1C]">{value.title}</h3>
<p className="text-[#5C5C5C] text-sm">{value.description}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6 text-center"></h2>
<div className="space-y-6">
{milestones.map((milestone, idx) => (
<motion.div
key={milestone.title}
initial={{ opacity: 0, x: -20 }}
animate={isContentInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.6 + idx * 0.1 }}
className="flex flex-col md:flex-row md:items-start gap-4 p-6 bg-[#FFFBF5] rounded-xl border border-[#E5E5E5]"
>
<div className="md:w-32 shrink-0">
<span className="text-sm font-medium text-[var(--color-brand-primary)]">{milestone.date}</span>
</div>
<div className="flex-1">
<h3 className="font-semibold text-[#1A1A2E] mb-1">{milestone.title}</h3>
<p className="text-[#718096] text-sm">{milestone.description}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.7 }}
className="bg-[#FFFBF5] rounded-2xl p-8 border border-[#E5E5E5]"
>
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6 text-center"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg">
<MapPin className="w-5 h-5 text-[var(--color-brand-primary)]" />
</div>
<div>
<p className="text-sm text-[#5C5C5C]"></p>
<p className="text-sm font-medium text-[#1C1C1C]">{COMPANY_INFO.address}</p>
</div>
{COMPANY_STATS.map((stat) => (
<div key={stat.label} className="text-center py-6 px-4 bg-[#F5F5F5] rounded-2xl border border-[#E5E5E5]">
<div className="text-3xl font-bold bg-gradient-to-r from-[var(--color-brand-primary)] to-[#E85D75] bg-clip-text text-transparent mb-1">
<CountUp end={stat.value} duration={2000} />
{stat.suffix}
</div>
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg">
<Mail className="w-5 h-5 text-[var(--color-brand-primary)]" />
</div>
<div>
<p className="text-sm text-[#5C5C5C]"></p>
<a href={`mailto:${COMPANY_INFO.email}`} className="text-sm font-medium text-[#1C1C1C] hover:text-[var(--color-brand-primary)] transition-colors">
{COMPANY_INFO.email}
</a>
</div>
</div>
<div className="text-sm text-[#5C5C5C]">{stat.label}</div>
</div>
</motion.div>
))}
</motion.div>
{/* Bottom CTA */}
<div className="mt-16 text-center py-16 bg-[#F5F5F5] rounded-2xl">
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<Button size="lg" className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white" asChild>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
{/* 核心价值观 - StaggerContainer */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16" staggerDelay={0.12}>
{VALUES.map((value) => {
const Icon = value.icon;
return (
<StaggerItem key={value.title}>
<div className="bg-white rounded-xl p-6 border border-[#E5E5E5] text-center hover:border-[var(--color-brand-primary)]/20 hover:shadow-md transition-all duration-300">
<div className="w-10 h-10 bg-[var(--color-brand-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{value.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{value.description}</p>
</div>
</StaggerItem>
);
})}
</StaggerContainer>
{/* 团队优势 - StaggerContainer */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16" staggerDelay={0.1}>
{TEAM_PILLARS.map((item, idx) => {
const Icon = item.icon;
return (
<StaggerItem key={item.title}>
<div className={`bg-white rounded-xl p-6 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-md transition-all duration-300 h-full ${idx >= 3 ? 'md:col-span-1 lg:col-start-1' : ''}`}>
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center mb-4">
<Icon className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{item.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{item.description}</p>
</div>
</StaggerItem>
);
})}
</StaggerContainer>
{/* CTA */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
className="text-center py-16 bg-[#F5F5F5] rounded-2xl"
>
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<StaticLink href="/contact">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white rounded-lg text-sm font-medium transition-colors">
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</div>
</div>
+1 -1
View File
@@ -97,7 +97,7 @@ jest.mock('@/lib/constants', () => ({
{ value: '10+', label: '企业客户' },
{ value: '20+', label: '成功案例' },
{ value: '30+', label: '项目交付' },
{ value: '12+', label: '年行业经验' },
{ value: '12+', label: '年核心成员行业经验' },
],
}));
+172 -190
View File
@@ -1,6 +1,5 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -13,6 +12,8 @@ import {
Award,
TrendingUp,
} from 'lucide-react';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
interface CaseKeyMoment {
title: string;
@@ -39,17 +40,11 @@ interface CaseItem {
slug: string;
date: string;
image?: string;
/** 客户面临的挑战 */
challenge: string;
/** 我们的解决方案 */
solution: string;
/** 关键时刻 */
keyMoments: CaseKeyMoment[];
/** 成果数据 */
results: CaseResult[];
/** 客户证言 */
testimonial?: CaseTestimonial;
/** 合作时长 */
duration: string;
}
@@ -58,32 +53,13 @@ interface CaseDetailClientProps {
}
export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
const [isVisible, setIsVisible] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (contentRef.current) {
observer.observe(contentRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div className="min-h-screen bg-white">
{/* Hero - InkReveal */}
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<BackButton />
<div className="max-w-4xl mt-8">
<InkReveal className="max-w-4xl mt-8">
<Badge className="mb-4 bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/20">
{caseItem.category}
</Badge>
@@ -91,193 +67,199 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
{caseItem.title}
</h1>
<p className="text-lg text-[#5C5C5C]">{caseItem.excerpt}</p>
</div>
</InkReveal>
</div>
</div>
<div ref={contentRef} className="container-wide py-12 md:py-16">
<div
className={`
grid lg:grid-cols-3 gap-8 lg:gap-12
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up' : ''}
`}
>
<div className="container-wide py-12 md:py-16">
<div className="grid lg:grid-cols-3 gap-8 lg:gap-12">
<div className="lg:col-span-2 space-y-12">
{/* 客户遇到的成长瓶颈 */}
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-white" />
{/* 客户遇到的成长瓶颈 - ScrollReveal */}
<ScrollReveal>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.challenge}
</p>
</section>
{/* 我们如何智连未来 */}
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Target className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="prose prose-base max-w-none [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-[#1C1C1C] [&_h3]:mt-8 [&_h3]:mb-4 [&_p]:text-[#5C5C5C] [&_p]:leading-[1.8] [&_p]:mb-4 [&_p]:text-base">
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.solution}
{caseItem.challenge}
</p>
</div>
</section>
{/* 共同成长的故事 */}
{caseItem.keyMoments && caseItem.keyMoments.length > 0 && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Clock className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="space-y-4">
{caseItem.keyMoments.map((moment, index) => (
<div
key={index}
className="p-4 bg-white rounded-lg border border-[#E5E5E5]"
>
<div className="flex items-start gap-3">
<Quote className="w-5 h-5 text-[var(--color-brand-primary)] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-[#1C1C1C] mb-2">
{moment.title}
</h4>
<p className="text-sm text-[#737373]">
{moment.description}
</p>
</div>
</div>
</div>
))}
</div>
</section>
)}
</ScrollReveal>
{/* 今天,他们走到了哪里 */}
{caseItem.results && caseItem.results.length > 0 && (
{/* 我们如何智连未来 - ScrollReveal */}
<ScrollReveal delay={0.1}>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Award className="w-6 h-6 text-white" />
<Target className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="grid sm:grid-cols-3 gap-4">
{caseItem.results.map((result, index) => (
<div
key={index}
className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[var(--color-brand-primary)] transition-colors"
>
<TrendingUp className="w-8 h-8 text-[var(--color-brand-primary)] mb-3" />
<div className="text-2xl font-semibold text-[var(--color-brand-primary)] mb-1">
{result.value}
</div>
<div className="text-sm text-[#737373]">
{result.label}
</div>
</div>
))}
</div>
</section>
)}
{/* 客户证言精选 */}
{caseItem.testimonial && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Quote className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5]">
<Quote className="w-8 h-8 text-[var(--color-brand-primary)] mb-4" />
<p className="text-lg text-[#1C1C1C] leading-relaxed mb-4">
{caseItem.testimonial.quote}
<div className="prose prose-base max-w-none [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-[#1C1C1C] [&_h3]:mt-8 [&_h3]:mb-4 [&_p]:text-[#5C5C5C] [&_p]:leading-[1.8] [&_p]:mb-4 [&_p]:text-base">
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.solution}
</p>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center">
<span className="text-white font-semibold"></span>
</div>
<div>
<p className="font-semibold text-[#1C1C1C]">
{caseItem.testimonial.author}
</p>
<p className="text-sm text-[#737373]">
{caseItem.testimonial.role}
</p>
</div>
</div>
</div>
</section>
</ScrollReveal>
{/* 共同成长的故事 - StaggerContainer */}
{caseItem.keyMoments && caseItem.keyMoments.length > 0 && (
<ScrollReveal delay={0.15}>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Clock className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<StaggerContainer className="space-y-4" staggerDelay={0.1}>
{caseItem.keyMoments.map((moment, index) => (
<StaggerItem key={index}>
<div className="p-4 bg-white rounded-lg border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-sm transition-all duration-300">
<div className="flex items-start gap-3">
<Quote className="w-5 h-5 text-[var(--color-brand-primary)] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-[#1C1C1C] mb-2">
{moment.title}
</h4>
<p className="text-sm text-[#737373]">
{moment.description}
</p>
</div>
</div>
</div>
</StaggerItem>
))}
</StaggerContainer>
</section>
</ScrollReveal>
)}
{/* 今天,他们走到了哪里 - StaggerContainer 数据指标 */}
{caseItem.results && caseItem.results.length > 0 && (
<ScrollReveal delay={0.2}>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Award className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<StaggerContainer className="grid sm:grid-cols-3 gap-4" staggerDelay={0.12}>
{caseItem.results.map((result, index) => (
<StaggerItem key={index}>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[var(--color-brand-primary)] hover:shadow-md transition-all duration-300 text-center">
<TrendingUp className="w-8 h-8 text-[var(--color-brand-primary)] mb-3 mx-auto" />
<div className="text-2xl font-semibold text-[var(--color-brand-primary)] mb-1">
{result.value}
</div>
<div className="text-sm text-[#737373]">
{result.label}
</div>
</div>
</StaggerItem>
))}
</StaggerContainer>
</section>
</ScrollReveal>
)}
{/* 客户证言精选 - ScrollReveal */}
{caseItem.testimonial && (
<ScrollReveal delay={0.25}>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center">
<Quote className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5]">
<Quote className="w-8 h-8 text-[var(--color-brand-primary)] mb-4" />
<p className="text-lg text-[#5C5C5C] leading-relaxed mb-4">
{caseItem.testimonial.quote}
</p>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center">
<span className="text-white font-semibold"></span>
</div>
<div>
<p className="font-semibold text-[#1C1C1C]">
{caseItem.testimonial.author}
</p>
<p className="text-sm text-[#737373]">
{caseItem.testimonial.role}
</p>
</div>
</div>
</div>
</section>
</ScrollReveal>
)}
</div>
{/* 侧边栏 */}
<div className="space-y-6">
<div className="p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5]">
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4">
</h3>
<dl className="space-y-3">
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.testimonial?.author || '客户企业'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.category}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.duration || '3年'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">{caseItem.date}</dd>
</div>
</dl>
</div>
<ScrollReveal delay={0.1}>
<div className="p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5]">
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4">
</h3>
<dl className="space-y-3">
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.testimonial?.author || '客户企业'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.category}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.duration || '3年'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">{caseItem.date}</dd>
</div>
</dl>
</div>
</ScrollReveal>
<div className="p-6 bg-gradient-to-br from-[var(--color-brand-primary)] to-[#8B1429] rounded-lg text-white">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-white/80 mb-4">
</p>
<Button
className="w-full bg-white text-[var(--color-brand-primary)] hover:bg-white/90"
asChild
>
<StaticLink href="/contact"></StaticLink>
</Button>
</div>
<ScrollReveal delay={0.2}>
<div className="p-6 bg-gradient-to-br from-[var(--color-brand-primary)] to-[#8B1429] rounded-lg text-white">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-white/80 mb-4">
</p>
<Button
className="w-full bg-white text-[var(--color-brand-primary)] hover:bg-white/90"
asChild
>
<StaticLink href="/contact"></StaticLink>
</Button>
</div>
</ScrollReveal>
</div>
</div>
</div>
+26 -38
View File
@@ -1,7 +1,6 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent } from 'react';
import { useInView } from 'framer-motion';
import { useState, useMemo, ChangeEvent } from 'react';
import { CASES } from '@/lib/constants';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
@@ -9,8 +8,9 @@ import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { Search, ArrowLeft, Building2, Calendar, TrendingUp, Filter, SearchX } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
import { Pagination } from '@/components/ui/pagination';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
const industries = ['全部', ...Array.from(new Set(CASES.map((c) => c.industry)))];
const ITEMS_PER_PAGE = 6;
@@ -19,8 +19,6 @@ export default function CasesPage() {
const [selectedIndustry, setSelectedIndustry] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredCases = useMemo(() => {
return CASES.filter((caseItem) => {
@@ -60,14 +58,10 @@ export default function CasesPage() {
description="我们与优秀的企业同行,共同成长,共创未来"
/>
<div className="container-wide relative z-10 py-16" ref={contentRef} id="page-content">
<div className="container-wide relative z-10 py-16" ref={undefined} id="page-content">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-8 space-y-4"
>
{/* 筛选区 - InkReveal */}
<InkReveal className="mb-8 space-y-4">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex items-center gap-2 text-[#1C1C1C]">
<Filter className="w-5 h-5" />
@@ -102,7 +96,7 @@ export default function CasesPage() {
aria-label="搜索案例"
/>
</div>
</motion.div>
</InkReveal>
{paginatedCases.length === 0 ? (
<div className="text-center py-20">
@@ -119,14 +113,10 @@ export default function CasesPage() {
</div>
) : (
<>
<div className="grid md:grid-cols-2 gap-8">
{paginatedCases.map((caseItem, index) => (
<motion.div
key={caseItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
{/* 卡片网格 - StaggerContainer */}
<StaggerContainer className="grid md:grid-cols-2 gap-8" staggerDelay={0.1}>
{paginatedCases.map((caseItem) => (
<StaggerItem key={caseItem.id}>
<StaticLink
href={`/cases/${caseItem.id}`}
className="group bg-white rounded-2xl border border-[#E5E5E5] overflow-hidden hover:shadow-xl transition-all duration-300 block"
@@ -175,9 +165,9 @@ export default function CasesPage() {
</div>
</div>
</StaticLink>
</motion.div>
</StaggerItem>
))}
</div>
</StaggerContainer>
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} scrollTargetId="page-content" />
@@ -189,20 +179,17 @@ export default function CasesPage() {
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-[#F5F5F5] py-16"
>
<div className="container-wide text-center">
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-6">
</h2>
<p className="text-lg text-[#5C5C5C] mb-8 max-w-2xl mx-auto">
</p>
<Button
{/* CTA - ScrollReveal */}
<ScrollReveal>
<div className="bg-[#F5F5F5] py-16">
<div className="container-wide text-center">
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-6">
</h2>
<p className="text-lg text-[#5C5C5C] mb-8 max-w-2xl mx-auto">
</p>
<Button
size="lg"
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white"
asChild
@@ -212,8 +199,9 @@ export default function CasesPage() {
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</StaticLink>
</Button>
</div>
</div>
</motion.div>
</ScrollReveal>
</div>
);
}
+79 -93
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useRef, Suspense } from 'react';
import { useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
@@ -10,6 +10,8 @@ import { Toast } from '@/components/ui/toast';
import { Mail, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { trackContactForm, trackConversion } from '@/lib/analytics';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
@@ -32,7 +34,6 @@ interface FormErrors {
function ContactFormContent() {
const searchParams = useSearchParams();
const isSuccessFromRedirect = searchParams.get('success') === 'true';
const [isVisible, setIsVisible] = useState(false);
const [showToast, setShowToast] = useState(isSuccessFromRedirect);
const [toastMessage, setToastMessage] = useState(
isSuccessFromRedirect ? '表单提交成功!我们会尽快与您联系。' : ''
@@ -50,13 +51,6 @@ function ContactFormContent() {
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
requestAnimationFrame(() => {
setIsVisible(true);
});
}, []);
const validateField = (field: keyof ContactFormData, value: string) => {
try {
@@ -162,18 +156,14 @@ function ContactFormContent() {
/>
)}
<section className="py-24 relative overflow-hidden" ref={sectionRef}>
<section className="py-24 relative overflow-hidden">
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-0" style={{ background: 'radial-gradient(ellipse at center, rgba(196,30,58,0.03) 0%, transparent 70%)' }} />
</div>
<div className="container-wide relative z-10">
<div
className={`
mb-16 opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up' : ''}
`}
>
{/* 标题区 - InkReveal */}
<InkReveal className="mb-16">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-px bg-linear-to-r from-[#1C1C1C] to-[var(--color-brand-primary)]" />
<span className="text-sm text-[#5C5C5C] tracking-wide" data-testid="page-badge"></span>
@@ -184,91 +174,87 @@ function ContactFormContent() {
<p className="mt-4 text-[#5C5C5C] max-w-2xl" data-testid="page-description">
</p>
</div>
</InkReveal>
<div className="grid lg:grid-cols-5 gap-12 lg:gap-16">
<div
className={`
lg:col-span-2 space-y-8 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
`}
>
<div>
<h2 className="text-lg font-semibold text-[#1C1C1C] mb-6"></h2>
<div className="space-y-4" data-testid="contact-info">
<div className="flex items-start gap-4 group" data-testid="email-info">
<div className="w-10 h-10 bg-[var(--color-brand-primary)] rounded-md flex items-center justify-center shrink-0 transition-transform duration-200 group-hover:scale-105">
<Mail className="w-5 h-5 text-white" />
{/* 左侧联系信息 - StaggerContainer */}
<StaggerContainer className="lg:col-span-2 space-y-8 flex flex-col" staggerDelay={0.12}>
<StaggerItem>
<div>
<h2 className="text-lg font-semibold text-[#1C1C1C] mb-6"></h2>
<div className="space-y-4" data-testid="contact-info">
<div className="flex items-start gap-4 group" data-testid="email-info">
<div className="w-10 h-10 bg-[var(--color-brand-primary)] rounded-md flex items-center justify-center shrink-0 transition-transform duration-200 group-hover:scale-105">
<Mail className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<a href={`mailto:${COMPANY_INFO.email}`} className="text-[#1C1C1C] hover:text-[var(--color-brand-primary)] transition-colors duration-200" data-testid="email-link">
{COMPANY_INFO.email}
</a>
</div>
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<a href={`mailto:${COMPANY_INFO.email}`} className="text-[#1C1C1C] hover:text-[var(--color-brand-primary)] transition-colors duration-200" data-testid="email-link">
{COMPANY_INFO.email}
</a>
</div>
</div>
<div className="flex items-start gap-4 group" data-testid="address-info">
<div className="w-10 h-10 bg-[var(--color-brand-primary)] rounded-md flex items-center justify-center shrink-0 transition-transform duration-200 group-hover:scale-105">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<p className="text-[#1C1C1C]" data-testid="address-text">{COMPANY_INFO.address}</p>
<div className="flex items-start gap-4 group" data-testid="address-info">
<div className="w-10 h-10 bg-[var(--color-brand-primary)] rounded-md flex items-center justify-center shrink-0 transition-transform duration-200 group-hover:scale-105">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-[#5C5C5C] mb-1"></p>
<p className="text-[#1C1C1C]" data-testid="address-text">{COMPANY_INFO.address}</p>
</div>
</div>
</div>
</div>
</div>
</StaggerItem>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]" data-testid="work-hours-card">
<div className="flex items-center gap-2 mb-3">
<Clock className="w-4 h-4 text-[var(--color-brand-primary)]" />
<h2 className="text-sm font-medium text-[#1C1C1C]"></h2>
</div>
<div className="space-y-1">
<div className="flex justify-between text-sm" data-testid="work-hours-row">
<span className="text-[#5C5C5C]"></span>
<span className="text-[var(--color-brand-primary)]">9:00 - 18:00</span>
<StaggerItem>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]" data-testid="work-hours-card">
<div className="flex items-center gap-2 mb-3">
<Clock className="w-4 h-4 text-[var(--color-brand-primary)]" />
<h2 className="text-sm font-medium text-[#1C1C1C]"></h2>
</div>
<div className="space-y-1">
<div className="flex justify-between text-sm" data-testid="work-hours-row">
<span className="text-[#5C5C5C]"></span>
<span className="text-[var(--color-brand-primary)]">9:00 - 18:00</span>
</div>
</div>
</div>
</div>
</StaggerItem>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]">
<div className="flex items-center gap-2 mb-3">
<HeadphonesIcon className="w-4 h-4 text-[var(--color-brand-primary)]" />
<h2 className="text-sm font-medium text-[#1C1C1C]"></h2>
</div>
<div className="space-y-3">
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"> 2 </p>
<StaggerItem>
<div className="bg-[#FFFBF5] p-5 rounded-lg border border-[#E5E5E5]">
<div className="flex items-center gap-2 mb-3">
<HeadphonesIcon className="w-4 h-4 text-[var(--color-brand-primary)]" />
<h2 className="text-sm font-medium text-[#1C1C1C]"></h2>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
<div className="space-y-3">
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"> 2 </p>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
</div>
<div className="flex items-start gap-2">
<div className="w-1.5 h-1.5 bg-[var(--color-brand-primary)] rounded-full mt-2 shrink-0" />
<p className="text-sm text-[#5C5C5C]"></p>
</div>
</div>
</div>
</div>
</div>
</StaggerItem>
</StaggerContainer>
<div
className={`
lg:col-span-3 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-2' : ''}
`}
>
{/* 右侧表单 - ScrollReveal */}
<ScrollReveal delay={0.15}>
<div className="bg-[#F5F7FA] p-6 sm:p-8 rounded-lg border border-[#E2E8F0] flex-1 flex flex-col">
<h2 className="text-lg font-semibold text-[#1A1A2E] mb-6"></h2>
{isSubmitted ? (
<div className="text-center py-12 flex-1 flex items-center justify-center">
<div className="w-16 h-16 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center mx-auto mb-4 animate-stamp-in">
<div className="w-16 h-16 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-8 h-8 text-white" />
</div>
<h4 className="text-xl font-semibold text-[#1A1A2E] mb-2"></h4>
@@ -314,9 +300,9 @@ function ContactFormContent() {
required
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
/>
onBlur={(e) => handleBlur('email', e.target.value)}
error={errors.email}
/>
<Input
name="subject"
data-testid="subject-input"
@@ -325,10 +311,10 @@ function ContactFormContent() {
placeholder="请输入消息主题"
required
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
/>
onChange={(e) => handleChange('subject', e.target.value)}
onBlur={(e) => handleBlur('subject', e.target.value)}
error={errors.subject}
/>
<Textarea
name="message"
data-testid="message-input"
@@ -338,10 +324,10 @@ function ContactFormContent() {
rows={5}
required
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
/>
onChange={(e) => handleChange('message', e.target.value)}
onBlur={(e) => handleBlur('message', e.target.value)}
error={errors.message}
/>
<Button
type="submit"
data-testid="submit-button"
@@ -364,7 +350,7 @@ function ContactFormContent() {
</form>
)}
</div>
</div>
</ScrollReveal>
</div>
</div>
</section>
+7 -12
View File
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { HeroSection } from "@/components/sections/hero-section";
import { SectionSkeleton } from "@/components/ui/loading-skeleton";
import { AnimatedInkDivider } from "@/components/ui/animated-ink-divider";
import type { ReactNode } from 'react';
declare global {
@@ -102,23 +103,17 @@ function HomeContent({ heroStats }: { heroStats: ReactNode }) {
return (
<main id="main-content" className="min-h-screen bg-white dark:bg-(--color-bg-primary)">
<HeroSection heroStats={heroStats} />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<ServicesSection />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<HomeSolutionsSection />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<ProductsSection />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<AboutSection />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<TeamSection />
{/* 墨韵分割线 */}
<div className="ink-divider" />
<AnimatedInkDivider />
<NewsSection />
</main>
);
+31 -38
View File
@@ -1,7 +1,6 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent } from 'react';
import { useInView } from 'framer-motion';
import { useState, useMemo, ChangeEvent } from 'react';
import { NEWS } from '@/lib/constants';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -10,8 +9,9 @@ import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { Search, Calendar, Filter, ArrowRight, SearchX } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
import { Pagination } from '@/components/ui/pagination';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
const categories = ['全部', '公司新闻', '产品发布', '研发动态'];
const ITEMS_PER_PAGE = 9;
@@ -20,8 +20,7 @@ export default function NewsListPage() {
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
@@ -60,13 +59,9 @@ export default function NewsListPage() {
description="了解睿新致远最新动态,把握行业发展脉搏"
/>
<div className="container-wide relative z-10 py-12" ref={contentRef} id="page-content">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-8 space-y-4"
>
<div className="container-wide relative z-10 py-12" id="page-content">
{/* 筛选区 - InkReveal */}
<InkReveal className="mb-8 space-y-4">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex items-center gap-2 text-[#1C1C1C]">
<Filter className="w-5 h-5" />
@@ -101,7 +96,7 @@ export default function NewsListPage() {
aria-label="搜索新闻"
/>
</div>
</motion.div>
</InkReveal>
{paginatedNews.length === 0 ? (
<div className="text-center py-20">
@@ -118,14 +113,10 @@ export default function NewsListPage() {
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedNews.map((newsItem, index) => (
<motion.div
key={newsItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
>
{/* 卡片网格 - StaggerContainer */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" staggerDelay={0.08}>
{paginatedNews.map((newsItem) => (
<StaggerItem key={newsItem.id}>
<StaticLink href={`/news/${newsItem.id}`}>
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer border-[#E5E5E5] hover:border-[var(--color-brand-primary)]">
<CardContent className="p-0">
@@ -164,9 +155,9 @@ export default function NewsListPage() {
</CardContent>
</Card>
</StaticLink>
</motion.div>
</StaggerItem>
))}
</div>
</StaggerContainer>
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} scrollTargetId="page-content" />
@@ -176,21 +167,23 @@ export default function NewsListPage() {
</>
)}
{/* Bottom CTA */}
<div className="mt-12 text-center py-16 bg-[#F5F5F5] rounded-2xl">
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<Button size="lg" className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white" asChild>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
{/* CTA - ScrollReveal */}
<ScrollReveal>
<div className="mt-12 text-center py-16 bg-[#F5F5F5] rounded-2xl">
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<Button size="lg" className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white" asChild>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
</ScrollReveal>
</div>
</div>
);
+36 -47
View File
@@ -1,7 +1,6 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent } from 'react';
import { useInView } from 'framer-motion';
import { useState, useMemo, ChangeEvent } from 'react';
import { SERVICES } from '@/lib/constants';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -9,8 +8,9 @@ import { Input } from '@/components/ui/input';
import { PageHeader } from '@/components/ui/page-header';
import { Search, ArrowLeft, Code, BarChart3, Lightbulb, Puzzle, Filter, SearchX } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
import { Pagination } from '@/components/ui/pagination';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Code,
@@ -26,8 +26,7 @@ export default function ServicesPage() {
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredServices = useMemo(() => {
return SERVICES.filter((service) => {
const matchesCategory = selectedCategory === '全部' || service.title.includes(selectedCategory);
@@ -66,14 +65,10 @@ export default function ServicesPage() {
description="专业技术团队,为您提供全方位的数字化解决方案"
/>
<div className="container-wide relative z-10 py-16" ref={contentRef} id="page-content">
<div className="container-wide relative z-10 py-16" id="page-content">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-8 space-y-4"
>
{/* 筛选区 - InkReveal */}
<InkReveal className="mb-8 space-y-4">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex items-center gap-2 text-[#1C1C1C]">
<Filter className="w-5 h-5" />
@@ -108,7 +103,7 @@ export default function ServicesPage() {
aria-label="搜索服务"
/>
</div>
</motion.div>
</InkReveal>
{paginatedServices.length === 0 ? (
<div className="text-center py-20">
@@ -125,16 +120,12 @@ export default function ServicesPage() {
</div>
) : (
<>
<div className="grid md:grid-cols-2 gap-8">
{paginatedServices.map((service, index) => {
{/* 卡片网格 - StaggerContainer */}
<StaggerContainer className="grid md:grid-cols-2 gap-8" staggerDelay={0.1}>
{paginatedServices.map((service) => {
const Icon = iconMap[service.icon];
return (
<motion.div
key={service.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<StaggerItem key={service.id}>
<StaticLink
href={`/services/${service.id}`}
className="group bg-white rounded-2xl border border-[#E5E5E5] overflow-hidden hover:shadow-xl transition-all duration-300 block h-full"
@@ -169,10 +160,10 @@ export default function ServicesPage() {
</div>
</div>
</StaticLink>
</motion.div>
</StaggerItem>
);
})}
</div>
</StaggerContainer>
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} scrollTargetId="page-content" />
@@ -184,31 +175,29 @@ export default function ServicesPage() {
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-[#F5F5F5] py-16"
>
<div className="container-wide text-center">
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-6">
</h2>
<p className="text-lg text-[#5C5C5C] mb-8 max-w-2xl mx-auto">
</p>
<Button
size="lg"
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white"
asChild
>
<StaticLink href="/contact">
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</StaticLink>
</Button>
{/* CTA - ScrollReveal */}
<ScrollReveal>
<div className="bg-[#F5F5F5] py-16">
<div className="container-wide text-center">
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-6">
</h2>
<p className="text-lg text-[#5C5C5C] mb-8 max-w-2xl mx-auto">
</p>
<Button
size="lg"
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white"
asChild
>
<StaticLink href="/contact">
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</StaticLink>
</Button>
</div>
</div>
</motion.div>
</ScrollReveal>
</div>
);
}
+90 -100
View File
@@ -1,41 +1,55 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { useRef, type MouseEvent } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { RippleButton } from '@/components/ui/ripple-button';
import { Shield, Building2, Users, Code, Target, ArrowRight } from 'lucide-react';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { TextReveal } from '@/components/ui/scroll-animations';
const TEAM_PILLARS = [
{
icon: Shield,
title: '12+ 年行业深耕',
description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业,积累了丰富的跨行业经验和最佳实践。',
},
{
icon: Building2,
title: '大型 IT 企业背景',
description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识,确保每一个项目都经得起考验。',
},
{
icon: Users,
title: '复合型技术团队',
description: '我们的团队成员既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案,而非纸上谈兵。',
},
{
icon: Code,
title: '全栈技术能力',
description: '团队掌握从前端到后端、从云原生到数据智能、从移动端到物联网的全栈技术能力,能够应对各种复杂的技术挑战。',
},
{
icon: Target,
title: '结果导向交付',
description: '我们不以"项目上线"为终点,而是以"客户业务是否真正改善"为衡量标准。每一个交付成果,都追求可量化的业务价值。',
},
{ icon: Shield, title: '12+ 年行业深耕', description: '核心团队长期从事技术咨询、企业数字化等领域,服务覆盖金融、制造、零售、政务、农业等多个行业,积累了丰富的跨行业经验和最佳实践。' },
{ icon: Building2, title: '大型 IT 企业背景', description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力、规范化的交付流程和严格的质量意识,确保每一个项目都经得起考验。' },
{ icon: Users, title: '复合型技术团队', description: '我们的团队成员既懂技术又懂业务,能够深入理解客户的真实场景和痛点,提供真正可落地的解决方案,而非纸上谈兵。' },
{ icon: Code, title: '全栈技术能力', description: '团队掌握从前端到后端、从云原生到数据智能、从移动端到物联网的全栈技术能力,能够应对各种复杂的技术挑战。' },
{ icon: Target, title: '结果导向交付', description: '我们不以"项目上线"为终点,而是以"客户业务是否真正改善"为衡量标准。每一个交付成果,都追求可量化的业务价值。' },
];
/** 3D Tilt 卡片组件 */
function TiltCard({ children, className = '' }: { children: React.ReactNode; className?: string }) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [5, -5]), { stiffness: 300, damping: 30 });
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-5, 5]), { stiffness: 300, damping: 30 });
function handleMouse(e: MouseEvent<HTMLDivElement>) {
if (!ref.current) {return;}
const rect = ref.current.getBoundingClientRect();
x.set((e.clientX - rect.left) / rect.width - 0.5);
y.set((e.clientY - rect.top) / rect.height - 0.5);
}
function handleMouseLeave() {
x.set(0);
y.set(0);
}
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, transformPerspective: 800 }}
className={className}
>
{children}
</motion.div>
);
}
export function TeamClient() {
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
@@ -48,81 +62,57 @@ export function TeamClient() {
/>
<div ref={contentRef} className="container-wide py-12 md:py-16">
{/* 团队概述 - TextReveal + InkReveal */}
<InkReveal className="max-w-5xl mx-auto mb-12">
<div className="bg-[#FFFBF5] rounded-2xl p-8 md:p-12 border border-[#E5E5E5]">
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6 text-center"></h2>
<TextReveal
text="我们的核心团队长期从事技术咨询、企业数字化等行业,拥有 12 年以上的深厚积累。开发团队成员来自于多个大型传统 IT 企业,具备扎实的工程能力和规范化的交付经验。我们相信,优秀的技术咨询不仅需要过硬的技术能力,更需要深入理解客户的业务场景和真实需求。"
className="text-[#5C5C5C] leading-relaxed max-w-3xl mx-auto text-center"
delay={0.1}
/>
</div>
</InkReveal>
{/* 团队优势 - StaggerContainer + TiltCard */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16" staggerDelay={0.1}>
{TEAM_PILLARS.map((item, idx) => {
const Icon = item.icon;
return (
<StaggerItem key={item.title}>
<TiltCard className={idx >= 3 ? 'md:col-span-1 lg:col-start-1' : ''}>
<div className="bg-white rounded-xl p-6 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-lg transition-all duration-300 h-full">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center mb-4">
<Icon className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{item.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{item.description}</p>
</div>
</TiltCard>
</StaggerItem>
);
})}
</StaggerContainer>
{/* Bottom CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="max-w-5xl mx-auto"
transition={{ duration: 0.6, delay: 0.4 }}
className="text-center py-16 bg-[#F5F5F5] rounded-2xl"
>
{/* 团队概述 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="bg-[#FFFBF5] rounded-2xl p-8 md:p-12 border border-[#E5E5E5] mb-16"
>
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6 text-center"></h2>
<div className="space-y-4 max-w-3xl mx-auto text-center">
<p className="text-[#5C5C5C] leading-relaxed">
<span className="text-[var(--color-brand-primary)] font-medium"></span><span className="text-[var(--color-brand-primary)] font-medium"></span> 12
</p>
<p className="text-[#5C5C5C] leading-relaxed">
<span className="text-[var(--color-brand-primary)] font-medium"> IT </span>
</p>
<p className="text-[#5C5C5C] leading-relaxed">
</p>
</div>
</motion.div>
{/* 团队优势 */}
<div className="mb-16">
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-8 text-center"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{TEAM_PILLARS.map((item, idx) => {
const Icon = item.icon;
return (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + idx * 0.1 }}
className={idx >= 3 ? 'md:col-span-1 lg:col-start-1' : ''}
>
<div className="bg-white rounded-xl p-6 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-md transition-all duration-300 h-full">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-xl flex items-center justify-center mb-4">
<Icon className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{item.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{item.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
{/* Bottom CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="mt-16 text-center py-16 bg-[#F5F5F5] rounded-2xl"
>
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<Button size="lg" className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white" asChild>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</motion.div>
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<StaticLink href="/contact">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white rounded-lg text-sm font-medium transition-colors">
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</div>
</div>
+48 -59
View File
@@ -1,13 +1,14 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { RippleButton } from '@/components/ui/ripple-button';
import { COMPANY_INFO } from '@/lib/constants';
import { ArrowRight, Target, HeartHandshake, Award } from 'lucide-react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { InkReveal, BlurReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { TextReveal } from '@/components/ui/scroll-animations';
const VALUES = [
{ title: '务实', description: '不追逐风口,只做真正为客户创造价值的事。', icon: Target },
@@ -26,13 +27,8 @@ export function AboutSection() {
<div className="absolute inset-0 bg-[linear-gradient(rgba(28,28,28,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(28,28,28,0.02)_1px,transparent_1px)] bg-size-[40px_40px]" />
<div className="container-wide relative z-10">
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}
className="max-w-4xl mx-auto"
>
{/* 标题 */}
{/* 标题 - InkReveal 墨迹揭示 */}
<InkReveal className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="about-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
@@ -42,60 +38,53 @@ export function AboutSection() {
{COMPANY_INFO.slogan}
</p>
</div>
</InkReveal>
{/* 品牌理念引用 */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
className="bg-white rounded-2xl p-8 mb-12 border border-[#E5E5E5]"
>
<blockquote className="text-lg text-[#5C5C5C] leading-relaxed text-center mb-6 italic">
<p>&ldquo;&lsquo;&rsquo;&lsquo;&rsquo;&rdquo;</p>
</blockquote>
<p className="text-[#1C1C1C] font-medium text-center">
</p>
</motion.div>
{/* 品牌理念 - TextReveal 逐词揭示 */}
<TextReveal
text="企业需要的,不是一个高高在上的专家,也不是一个做完就跑的卖家,而是一个能坐下来、一起想办法的同行者。"
className="text-center text-lg text-[#5C5C5C] leading-relaxed mb-8 max-w-3xl mx-auto"
delay={0.1}
/>
{/* 核心价值观 */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.15 }}
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16"
>
{VALUES.map((value) => {
const Icon = value.icon;
return (
<div
key={value.title}
className="bg-white rounded-xl p-6 border border-[#E5E5E5] text-center"
>
<div className="w-10 h-10 bg-[var(--color-brand-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" />
{/* 核心理念强调 - BlurReveal */}
<BlurReveal delay={0.3} className="bg-white rounded-2xl p-8 mb-12 border border-[#E5E5E5]">
<p className="text-[#1C1C1C] font-medium text-center text-lg">
</p>
</BlurReveal>
{/* 核心价值观 - StaggerContainer 交错入场 */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16" staggerDelay={0.15}>
{VALUES.map((value) => {
const Icon = value.icon;
return (
<StaggerItem key={value.title}>
<div className="bg-white rounded-xl p-6 border border-[#E5E5E5] text-center hover:border-[var(--color-brand-primary)]/20 hover:shadow-md transition-all duration-300">
<div className="w-10 h-10 bg-[var(--color-brand-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{value.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{value.description}</p>
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{value.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{value.description}</p>
</div>
);
})}
</motion.div>
</StaggerItem>
);
})}
</StaggerContainer>
{/* CTA */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.5 }}
className="text-center"
>
<StaticLink href="/about">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 border border-[#E5E5E5] rounded-lg text-sm font-medium text-[#1C1C1C] hover:border-[var(--color-brand-primary)] hover:text-[var(--color-brand-primary)] transition-colors">
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
{/* CTA */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.5 }}
className="text-center"
>
<StaticLink href="/about">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 border border-[#E5E5E5] rounded-lg text-sm font-medium text-[#1C1C1C] hover:border-[var(--color-brand-primary)] hover:text-[var(--color-brand-primary)] transition-colors">
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</div>
</section>
@@ -1,11 +1,12 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { ArrowRight, Lightbulb, Cpu, Users } from 'lucide-react';
import { InkReveal, StaggerContainer, StaggerItem, CountUp } from '@/lib/animations';
import { ScrollReveal } from '@/components/ui/scroll-animations';
const SOLUTIONS_OVERVIEW = [
{
@@ -31,6 +32,13 @@ const SOLUTIONS_OVERVIEW = [
},
];
const SOLUTION_STATS = [
{ value: 5, label: '覆盖行业', suffix: '+' },
{ value: 6, label: '自研产品', suffix: '款' },
{ value: 12, label: '年核心成员行业经验', suffix: '' },
{ value: 98, label: '客户满意度', suffix: '%' },
];
export function HomeSolutionsSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -38,12 +46,8 @@ export function HomeSolutionsSection() {
return (
<section id="solutions" role="region" aria-labelledby="solutions-heading" className="py-24 bg-[#F5F5F5] relative overflow-hidden" ref={ref}>
<div className="container-wide relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
{/* 标题 - InkReveal 墨迹揭示 */}
<InkReveal className="text-center max-w-3xl mx-auto mb-16">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="solutions-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
@@ -51,18 +55,30 @@ export function HomeSolutionsSection() {
<p className="text-lg text-[#5C5C5C]">
</p>
</motion.div>
</InkReveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{SOLUTIONS_OVERVIEW.map((item, idx) => {
{/* 数据指标 - ScrollReveal 滚动驱动 + CountUp 数字滚动 */}
<ScrollReveal className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto mb-16">
{SOLUTION_STATS.map((stat) => (
<div
key={stat.label}
className="text-center py-6 px-4 bg-white rounded-2xl border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/20 hover:shadow-md transition-all duration-300"
>
<div className="text-3xl sm:text-4xl font-bold bg-gradient-to-r from-[var(--color-brand-primary)] to-[#E85D75] bg-clip-text text-transparent mb-1">
<CountUp end={stat.value} duration={2000} />
{stat.suffix}
</div>
<div className="text-sm text-[#5C5C5C]">{stat.label}</div>
</div>
))}
</ScrollReveal>
{/* 卡片 - StaggerContainer 交错入场 */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-3 gap-8" staggerDelay={0.15}>
{SOLUTIONS_OVERVIEW.map((item) => {
const Icon = item.icon;
return (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 + idx * 0.15 }}
>
<StaggerItem key={item.title}>
<div className="bg-white rounded-2xl p-8 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-lg transition-all duration-300 h-full flex flex-col">
<div className="w-14 h-14 bg-[var(--color-brand-primary)] rounded-2xl flex items-center justify-center mb-6">
<Icon className="w-7 h-7 text-white" />
@@ -79,10 +95,10 @@ export function HomeSolutionsSection() {
))}
</ul>
</div>
</motion.div>
</StaggerItem>
);
})}
</div>
</StaggerContainer>
<motion.div
initial={{ opacity: 0, y: 20 }}
+10 -19
View File
@@ -1,13 +1,13 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ArrowRight, Calendar } from 'lucide-react';
import { NEWS } from '@/lib/constants';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
export function NewsSection() {
const ref = useRef(null);
@@ -20,12 +20,8 @@ export function NewsSection() {
return (
<section id="news" role="region" aria-labelledby="news-heading" className="py-24 bg-[#F5F5F5]" ref={ref}>
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-center max-w-3xl mx-auto mb-16"
>
{/* 标题 - InkReveal 墨迹揭示 */}
<InkReveal className="text-center max-w-3xl mx-auto mb-16">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="news-heading" className="text-3xl sm:text-4xl lg:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
@@ -33,17 +29,12 @@ export function NewsSection() {
<p className="text-lg text-[#5C5C5C]">
</p>
</motion.div>
</InkReveal>
{displayedNews.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
{displayedNews.map((newsItem, idx) => (
<motion.div
key={newsItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: idx * 0.08 }}
>
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto" staggerDelay={0.1}>
{displayedNews.map((newsItem) => (
<StaggerItem key={newsItem.id}>
<Card className="h-full flex flex-col group cursor-pointer border-[#E5E5E5] hover:border-[#1C1C1C]">
<CardHeader>
<div className="flex items-center gap-2 mb-3">
@@ -70,9 +61,9 @@ export function NewsSection() {
</StaticLink>
</CardContent>
</Card>
</motion.div>
</StaggerItem>
))}
</div>
</StaggerContainer>
) : (
<div className="text-center py-12">
<p className="text-lg text-[#5C5C5C]"></p>
+77 -73
View File
@@ -1,16 +1,22 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { RippleButton } from '@/components/ui/ripple-button';
import { InkCard } from '@/lib/animations';
import { Badge } from '@/components/ui/badge';
import { ArrowRight, Check, TrendingUp } from 'lucide-react';
import { InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ArrowRight, Check, Database, Users, FileText, BarChart3, Layers, MessageSquare } from 'lucide-react';
import { PRODUCTS } from '@/lib/constants';
const productIcons: Record<string, React.ComponentType<{ className?: string }>> = {
erp: Layers,
crm: Users,
cms: FileText,
bi: BarChart3,
dss: Database,
oa: MessageSquare,
};
export function ProductsSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -20,103 +26,101 @@ export function ProductsSection() {
<div className="absolute top-1/2 left-0 w-[400px] h-[400px] bg-[rgba(196,30,58,0.03)] rounded-full blur-3xl" />
<div className="absolute top-1/3 right-0 w-[300px] h-[300px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-left max-w-3xl mx-auto mb-16"
>
{/* 标题 - InkReveal 墨迹揭示 */}
<InkReveal className="text-left max-w-3xl mx-auto mb-16">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="products-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
</h2>
<p className="text-lg text-[#5C5C5C]">
ERPCRMOA
</p>
</motion.div>
</InkReveal>
{PRODUCTS.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-stretch">
{PRODUCTS.map((product) => (
<InkCard
key={product.id}
className="group cursor-pointer rounded-xl border border-[#E5E5E5] bg-white p-0 overflow-hidden hover:border-[var(--color-brand-primary)] transition-colors"
>
<StaticLink href={`/products/${product.id}`}>
<Card className="h-full flex flex-col border-0 shadow-none bg-transparent">
<CardHeader>
<Badge variant="secondary" className="w-fit mb-3">
{product.category}
</Badge>
<CardTitle>{product.title}</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex flex-col">
<CardDescription className="text-base leading-relaxed mb-4 flex-1">
{product.description}
</CardDescription>
<div className="mb-4">
<p className="text-sm font-medium text-[#1C1C1C] mb-2"></p>
<div className="flex flex-wrap gap-1.5">
{product.features.slice(0, 4).map((feature, idx) => (
<span
key={idx}
className="inline-flex items-center text-xs px-2 py-1 bg-[#FAFAFA] text-[#3D3D3D] rounded border border-[#E5E5E5]"
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" staggerDelay={0.08}>
{PRODUCTS.map((product) => {
const Icon = productIcons[product.id] || Layers;
return (
<StaggerItem key={product.id}>
<StaticLink href={`/products/${product.id}`} className="block group">
<div className="relative rounded-2xl border border-[#E5E5E5] bg-white p-6 hover:border-[var(--color-brand-primary)]/30 hover:shadow-xl transition-all duration-500 overflow-hidden h-full">
{/* 悬停光泽效果 */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-brand-primary)]/[0.02] to-transparent" />
</div>
<div className="relative z-10">
{/* 图标 + 分类 */}
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 rounded-xl bg-[#F5F5F5] group-hover:bg-[var(--color-brand-primary)] flex items-center justify-center transition-colors duration-300">
<Icon className="w-6 h-6 text-[#5C5C5C] group-hover:text-white transition-colors duration-300" />
</div>
<span className="text-xs text-[#5C5C5C] bg-[#F5F5F5] px-2.5 py-1 rounded-full group-hover:bg-[var(--color-brand-primary)]/10 group-hover:text-[var(--color-brand-primary)] transition-colors duration-300">
{product.category}
</span>
</div>
{/* 标题 + 描述 */}
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2 group-hover:text-[var(--color-brand-primary)] transition-colors duration-300">
{product.title}
</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-4 line-clamp-2">
{product.description}
</p>
{/* 核心功能标签 */}
<div className="flex flex-wrap gap-1.5 mb-4">
{product.features.slice(0, 3).map((feature, idx) => (
<span
key={idx}
className="inline-flex items-center text-xs px-2 py-0.5 bg-[#FAFAFA] text-[#3D3D3D] rounded border border-[#E5E5E5] group-hover:border-[var(--color-brand-primary)]/15 transition-colors duration-300"
>
<Check className="w-3 h-3 mr-1 text-[var(--color-brand-primary)]" />
{feature}
</span>
))}
{product.features.length > 3 && (
<span className="text-xs text-[#5C5C5C] px-1 py-0.5">
+{product.features.length - 3}
</span>
)}
</div>
{/* CTA */}
<div className="flex items-center text-sm font-medium text-[var(--color-brand-primary)] opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300">
<ArrowRight className="ml-1.5 w-4 h-4 transition-transform group-hover:translate-x-1" />
</div>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-[#1C1C1C] mb-2 flex items-center">
<TrendingUp className="w-4 h-4 mr-1 text-[var(--color-brand-primary)]" />
</p>
<ul className="space-y-1">
{product.benefits.map((benefit, idx) => (
<li key={idx} className="text-xs text-[#5C5C5C] flex items-start">
<span className="text-[var(--color-brand-primary)] mr-1.5"></span>
{benefit}
</li>
))}
</ul>
</div>
<div className="w-full mt-auto px-4 py-2 text-center text-sm font-medium border border-[#E5E5E5] rounded-md group-hover:bg-[var(--color-brand-primary-hover)] group-hover:text-white group-hover:border-[var(--color-brand-primary-hover)] transition-colors">
<ArrowRight className="ml-2 w-4 h-4 inline" />
</div>
</CardContent>
</Card>
</StaticLink>
</InkCard>
))}
</div>
</div>
</StaticLink>
</StaggerItem>
);
})}
</StaggerContainer>
) : (
<div className="text-center py-12">
<p className="text-lg text-[#5C5C5C]"></p>
</div>
)}
<motion.div
{/* 定制化 CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="mt-20 text-center"
className="mt-16 text-center"
>
<div className="bg-white rounded-2xl p-12 border border-[#E2E8F0] relative overflow-hidden">
<div className="bg-[#F5F5F5] rounded-2xl p-10 border border-[#E5E5E5] relative overflow-hidden">
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 right-0 w-64 h-64 bg-[rgba(79,70,229,0.03)] rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="absolute top-0 right-0 w-48 h-48 bg-[rgba(196,30,58,0.03)] rounded-full blur-3xl" />
</div>
<div className="relative z-10">
<h3 className="text-2xl sm:text-3xl font-bold text-[#1A1A2E] mb-4">
<h3 className="text-2xl font-bold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#718096] mb-8 max-w-2xl mx-auto">
<p className="text-[#5C5C5C] mb-6 max-w-xl mx-auto">
</p>
<StaticLink href="/contact">
+28 -32
View File
@@ -1,13 +1,12 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Code, BarChart3, Lightbulb, Puzzle, ArrowRight } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { RippleButton } from '@/components/ui/ripple-button';
import { InkCard } from '@/lib/animations';
import { InkCard, InkReveal, StaggerContainer, StaggerItem } from '@/lib/animations';
import { SERVICES } from '@/lib/constants';
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -27,12 +26,8 @@ export function ServicesSection() {
<div className="absolute top-1/3 right-0 w-[300px] h-[300px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
{/* 标题 - InkReveal 墨迹揭示 */}
<InkReveal className="text-center max-w-3xl mx-auto mb-16">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="services-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
@@ -40,36 +35,37 @@ export function ServicesSection() {
<p className="text-lg text-[#5C5C5C] max-w-2xl">
</p>
</motion.div>
</InkReveal>
{SERVICES.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StaggerContainer className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6" staggerDelay={0.12}>
{SERVICES.map((service) => {
const Icon = iconMap[service.icon];
return (
<InkCard
key={service.id}
className="rounded-xl border border-[#E5E5E5] bg-white p-6 hover:border-[var(--color-brand-primary)] transition-colors"
>
<StaticLink href={`/services/${service.id}`}>
<Card className="p-0 h-full border-0 shadow-none bg-transparent group cursor-pointer">
<CardContent className="p-0">
<div className="w-12 h-12 rounded-xl bg-[#F5F5F5] flex items-center justify-center mb-4 group-hover:bg-[var(--color-brand-primary)] transition-all duration-300">
{Icon && <Icon className="w-6 h-6 text-[#1C1C1C] group-hover:text-white transition-colors" />}
</div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3 group-hover:text-[var(--color-brand-primary)] transition-colors">{service.title}</h3>
<p className="text-[#5C5C5C] text-sm leading-relaxed">{service.description}</p>
<div className="mt-4 flex items-center text-[var(--color-brand-primary)] text-sm font-medium opacity-0 md:group-hover:opacity-100 md:opacity-0 transition-opacity">
<ArrowRight className="ml-1 w-4 h-4" />
</div>
</CardContent>
</Card>
</StaticLink>
</InkCard>
<StaggerItem key={service.id}>
<InkCard
className="rounded-xl border border-[#E5E5E5] bg-white p-6 hover:border-[var(--color-brand-primary)] transition-colors"
>
<StaticLink href={`/services/${service.id}`}>
<Card className="p-0 h-full border-0 shadow-none bg-transparent group cursor-pointer">
<CardContent className="p-0">
<div className="w-12 h-12 rounded-xl bg-[#F5F5F5] flex items-center justify-center mb-4 group-hover:bg-[var(--color-brand-primary)] transition-all duration-300">
{Icon && <Icon className="w-6 h-6 text-[#1C1C1C] group-hover:text-white transition-colors" />}
</div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3 group-hover:text-[var(--color-brand-primary)] transition-colors">{service.title}</h3>
<p className="text-[#5C5C5C] text-sm leading-relaxed">{service.description}</p>
<div className="mt-4 flex items-center text-[var(--color-brand-primary)] text-sm font-medium opacity-0 md:group-hover:opacity-100 md:opacity-0 transition-opacity">
<ArrowRight className="ml-1 w-4 h-4" />
</div>
</CardContent>
</Card>
</StaticLink>
</InkCard>
</StaggerItem>
);
})}
</div>
</StaggerContainer>
) : (
<div className="text-center py-12">
<p className="text-lg text-[#5C5C5C]"></p>
+79 -52
View File
@@ -1,11 +1,11 @@
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { useRef, type MouseEvent } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { RippleButton } from '@/components/ui/ripple-button';
import { ArrowRight, Briefcase, GraduationCap, Target, Users } from 'lucide-react';
import { InkReveal, StaggerContainer, StaggerItem, CountUp } from '@/lib/animations';
const TEAM_MEMBERS = [
{
@@ -43,12 +43,46 @@ const TEAM_MEMBERS = [
];
const TEAM_STATS = [
{ value: '12+', label: '年团队经验' },
{ value: '80%', label: '本科及以上学历' },
{ value: '4', label: '核心服务' },
{ value: '5+', label: '行业覆盖' },
{ value: 12, label: '年+核心成员行业经验', suffix: '' },
{ value: 80, label: '%本科及以上学历', suffix: '' },
{ value: 4, label: '核心服务', suffix: '' },
{ value: 5, label: '行业+', suffix: '' },
];
/** 3D Tilt 卡片组件 */
function TiltCard({ children, className = '' }: { children: React.ReactNode; className?: string }) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [6, -6]), { stiffness: 300, damping: 30 });
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-6, 6]), { stiffness: 300, damping: 30 });
function handleMouse(e: MouseEvent<HTMLDivElement>) {
if (!ref.current) {return;}
const rect = ref.current.getBoundingClientRect();
x.set((e.clientX - rect.left) / rect.width - 0.5);
y.set((e.clientY - rect.top) / rect.height - 0.5);
}
function handleMouseLeave() {
x.set(0);
y.set(0);
}
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, transformPerspective: 800 }}
className={className}
>
{children}
</motion.div>
);
}
export function TeamSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -59,13 +93,8 @@ export function TeamSection() {
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
{/* 标题区 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
{/* 标题区 - InkReveal 墨迹揭示 */}
<InkReveal className="text-center max-w-3xl mx-auto mb-16">
<div className="w-16 h-1 bg-[var(--color-brand-primary)] rounded-full mb-6" />
<h2 id="team-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
@@ -73,9 +102,9 @@ export function TeamSection() {
<p className="text-lg text-[#5C5C5C] leading-relaxed">
IT企业的核心团队
</p>
</motion.div>
</InkReveal>
{/* 团队数据概览 */}
{/* 团队数据概览 - CountUp 数字滚动 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
@@ -88,55 +117,53 @@ export function TeamSection() {
className="text-center py-4 px-3 bg-white rounded-xl border border-[#E5E5E5]"
>
<div className="text-2xl sm:text-3xl font-bold bg-gradient-to-r from-[var(--color-brand-primary)] to-[#E85D75] bg-clip-text text-transparent mb-1">
{stat.value}
<CountUp end={stat.value} duration={2000} />
{stat.suffix}
</div>
<div className="text-xs sm:text-sm text-[#5C5C5C]">{stat.label}</div>
</div>
))}
</motion.div>
{/* 团队成员卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto mb-12">
{TEAM_MEMBERS.map((member, idx) => {
{/* 团队成员卡片 - StaggerContainer + 3D Tilt */}
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto mb-12" staggerDelay={0.12}>
{TEAM_MEMBERS.map((member) => {
const Icon = member.icon;
return (
<motion.div
key={member.name}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + idx * 0.12 }}
>
<div className="bg-white rounded-2xl p-6 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-lg transition-all duration-300 h-full flex flex-col group">
{/* 头像区 */}
<div className="flex items-center gap-4 mb-4">
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${member.accentColor} flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300`}>
<Icon className="w-7 h-7 text-white" />
<StaggerItem key={member.name}>
<TiltCard className="h-full">
<div className="bg-white rounded-2xl p-6 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-lg transition-all duration-300 h-full flex flex-col group">
{/* 头像区 */}
<div className="flex items-center gap-4 mb-4">
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${member.accentColor} flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300`}>
<Icon className="w-7 h-7 text-white" />
</div>
<div className="min-w-0">
<h3 className="text-base font-bold text-[#1C1C1C] truncate">{member.name}</h3>
<span className="text-xs text-[var(--color-brand-primary)] font-medium">{member.initials}</span>
</div>
</div>
<div className="min-w-0">
<h3 className="text-base font-bold text-[#1C1C1C] truncate">{member.name}</h3>
<span className="text-xs text-[var(--color-brand-primary)] font-medium">{member.initials}</span>
{/* 简介 */}
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-4 flex-1">{member.bio}</p>
{/* 专业领域标签 */}
<div className="flex flex-wrap gap-1.5">
{member.specialties.map((spec) => (
<span
key={spec}
className="inline-flex items-center text-xs px-2.5 py-1 bg-[#FAFAFA] text-[#3D3D3D] rounded-full border border-[#E5E5E5] group-hover:border-[var(--color-brand-primary)]/20 transition-colors"
>
{spec}
</span>
))}
</div>
</div>
{/* 简介 */}
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-4 flex-1">{member.bio}</p>
{/* 专业领域标签 */}
<div className="flex flex-wrap gap-1.5">
{member.specialties.map((spec) => (
<span
key={spec}
className="inline-flex items-center text-xs px-2.5 py-1 bg-[#FAFAFA] text-[#3D3D3D] rounded-full border border-[#E5E5E5] group-hover:border-[var(--color-brand-primary)]/20 transition-colors"
>
{spec}
</span>
))}
</div>
</div>
</motion.div>
</TiltCard>
</StaggerItem>
);
})}
</div>
</StaggerContainer>
{/* CTA */}
<motion.div
@@ -0,0 +1,64 @@
'use client';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
/**
* AnimatedInkDivider - 墨韵分割线(滚动展开动画版)
*
* 替代静态的 .ink-divider CSS 类。
* 线条从中心向两侧展开,中心圆点弹性出现。
*
* @param className - 额外的 CSS 类名
* @param delay - 动画延迟(秒)
*/
export function AnimatedInkDivider({ className = '', delay = 0 }: { className?: string; delay?: number }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-80px' });
return (
<div ref={ref} className={`relative py-6 flex items-center justify-center ${className}`}>
{/* 左侧线条 - 从中心向左展开 */}
<motion.div
className="h-px flex-1 max-w-[200px] bg-gradient-to-r from-transparent to-[#1C1C1C]/20"
initial={{ scaleX: 0, originX: 1 }}
animate={isInView ? { scaleX: 1 } : {}}
transition={{ duration: 0.8, delay, ease: [0.16, 1, 0.3, 1] }}
/>
{/* 中心装饰 - 圆点 + 外圈 */}
<motion.div
className="relative mx-4 flex items-center justify-center"
initial={{ scale: 0 }}
animate={isInView ? { scale: 1 } : {}}
transition={{
type: 'spring',
stiffness: 300,
damping: 20,
delay: delay + 0.2,
}}
>
{/* 外圈光晕 */}
<motion.div
className="absolute w-5 h-5 rounded-full bg-[var(--color-brand-primary)]/10"
initial={{ scale: 0 }}
animate={isInView ? { scale: 1 } : {}}
transition={{
type: 'spring',
stiffness: 200,
damping: 15,
delay: delay + 0.35,
}}
/>
{/* 中心实心圆 */}
<div className="w-2 h-2 rounded-full bg-[var(--color-brand-primary)]" />
</motion.div>
{/* 右侧线条 - 从中心向右展开 */}
<motion.div
className="h-px flex-1 max-w-[200px] bg-gradient-to-l from-transparent to-[#1C1C1C]/20"
initial={{ scaleX: 0, originX: 0 }}
animate={isInView ? { scaleX: 1 } : {}}
transition={{ duration: 0.8, delay, ease: [0.16, 1, 0.3, 1] }}
/>
</div>
);
}
+1 -1
View File
@@ -6,6 +6,6 @@ export interface StatItem {
export const STATS: StatItem[] = [
{ value: '12+', label: '年核心成员行业经验' },
{ value: '4', label: '核心服务' },
{ value: '4', label: '自研产品' },
{ value: '6', label: '自研产品' },
{ value: '5+', label: '行业覆盖' },
];