feat: add TestimonialBlock, TestimonialSection, and CTASection components

This commit is contained in:
张翔
2026-04-30 19:19:28 +08:00
parent a6e1761b3d
commit 7d514d4b51
3 changed files with 182 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
'use client';
import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
interface CTASectionProps {
title?: string;
description?: string;
primaryLabel?: string;
primaryHref?: string;
secondaryLabel?: string;
secondaryHref?: string;
}
export function CTASection({
title = '开启您的数字化转型之旅',
description = '与诺瓦隆一起,让技术成为您业务增长的核心引擎',
primaryLabel = '立即咨询',
primaryHref = '/contact',
secondaryLabel = '查看案例',
secondaryHref = '/cases',
}: CTASectionProps) {
return (
<section className="bg-[#1C1C1C] py-16 md:py-24">
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="text-center max-w-3xl mx-auto"
>
<h2 className="text-3xl sm:text-4xl font-semibold text-white mb-4">
{title}
</h2>
<p className="text-lg text-[#A0A0A0] mb-10">
{description}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button size="lg" asChild>
<StaticLink href={primaryHref}>
{primaryLabel}
<ArrowRight className="w-4 h-4 ml-2" />
</StaticLink>
</Button>
<Button size="lg" variant="outline" className="border-white/30 text-white hover:bg-white/10" asChild>
<StaticLink href={secondaryHref}>
{secondaryLabel}
</StaticLink>
</Button>
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,86 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { TestimonialBlock } from '@/components/ui/testimonial-block';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const TESTIMONIALS = [
{
quote: '诺瓦隆的ERP系统帮助我们实现了财务与业务的深度一体化,运营效率提升了35%,数据决策更加精准。',
author: '张总',
title: 'CIO',
company: '某制造集团',
},
{
quote: 'CRM系统让我们的销售团队从繁杂的手工跟进中解放出来,客户转化率提升了28%,团队协作更高效。',
author: '李总',
title: '销售总监',
company: '某零售企业',
},
{
quote: 'BI平台让我们第一次真正实现了数据驱动决策,管理层可以随时掌握业务全貌,决策速度提升了60%。',
author: '王总',
title: 'VP of Data',
company: '某教育集团',
},
];
export function TestimonialSection() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => observer.disconnect();
}, []);
return (
<section
ref={sectionRef}
className="bg-[#FFFBF5] py-16 md:py-24"
>
<div className="container-wide">
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="mb-12"
>
<h2 className="text-3xl sm:text-4xl font-semibold text-[#1C1C1C] mb-4">
</h2>
<p className="text-lg text-[#595959] max-w-2xl">
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{TESTIMONIALS.map((testimonial, index) => (
<TestimonialBlock
key={testimonial.author}
quote={testimonial.quote}
author={testimonial.author}
title={testimonial.title}
company={testimonial.company}
index={index}
/>
))}
</div>
</div>
</section>
);
}
+38
View File
@@ -0,0 +1,38 @@
'use client';
import { motion } from 'framer-motion';
import { Quote } from 'lucide-react';
interface TestimonialBlockProps {
quote: string;
author: string;
title: string;
company: string;
index: number;
}
export function TestimonialBlock({ quote, author, title, company, index }: TestimonialBlockProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1, ease: [0.16, 1, 0.3, 1] }}
className="p-6 rounded-xl bg-white border border-[#E5E5E5] hover:shadow-md transition-shadow duration-300"
>
<Quote className="w-8 h-8 text-[#C41E3A]/20 mb-4" />
<blockquote className="text-[#3D3D3D] leading-relaxed mb-6">
&ldquo;{quote}&rdquo;
</blockquote>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#FEF2F4] flex items-center justify-center text-[#C41E3A] font-semibold text-sm">
{author.charAt(0)}
</div>
<div>
<div className="text-sm font-semibold text-[#1C1C1C]">{author}</div>
<div className="text-xs text-[#595959]">{title}{company}</div>
</div>
</div>
</motion.div>
);
}