feat: V2/V3组件体系与水墨雅致视觉改造
- 新增detail-v2组件库(HeroV2/V3, TrustSectionV2, CTASectionV2等) - Hero组件水墨雅致改造:浅色宣纸底/深色墨色文字/墨韵纹理 - 方案卡片添加推荐组合徽标(Package图标) - 独立产品页从V1组件迁移到V2/V3 - 修复why-us-section未使用导入和ESLint引号转义错误
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import type { CaseStudy } from '@/lib/constants/products';
|
||||
|
||||
interface CaseStudyCardProps {
|
||||
study: CaseStudy;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export function CaseStudyCard({ study, index }: CaseStudyCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-[#C41E3A]">
|
||||
{study.industry}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{study.client}</span>
|
||||
</div>
|
||||
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-900">挑战</h4>
|
||||
<p className="mb-4 text-sm leading-relaxed text-gray-600">{study.challenge}</p>
|
||||
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-900">解决方案</h4>
|
||||
<p className="mb-4 text-sm leading-relaxed text-gray-600">{study.solution}</p>
|
||||
|
||||
<div className="rounded-lg bg-green-50 p-3">
|
||||
<p className="text-sm font-medium text-green-800">{study.result}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Award, ExternalLink } from 'lucide-react';
|
||||
import type { Certification } from '@/lib/constants/products';
|
||||
|
||||
interface CertificationListProps {
|
||||
certifications: Certification[];
|
||||
}
|
||||
|
||||
export function CertificationList({ certifications }: CertificationListProps) {
|
||||
if (certifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{certifications.map((cert, index) => (
|
||||
<motion.a
|
||||
key={cert.name}
|
||||
href={cert.link || '#'}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 shadow-sm transition-all hover:border-gray-300 hover:shadow-md"
|
||||
>
|
||||
<Award className="h-4 w-4 text-[#C41E3A]" />
|
||||
<span className="font-medium">{cert.name}</span>
|
||||
<span className="text-xs text-gray-400">{cert.issuer}</span>
|
||||
{cert.link && cert.link !== '#' && (
|
||||
<ExternalLink className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, Package, Lightbulb, Wrench } from 'lucide-react';
|
||||
import type { CrossReference } from '@/lib/constants/cross-references';
|
||||
|
||||
interface CrossRecommendGridProps {
|
||||
items: CrossReference[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const typeConfig = {
|
||||
product: {
|
||||
icon: Package,
|
||||
label: '产品',
|
||||
color: '#3b82f6',
|
||||
bg: '#eff6ff',
|
||||
},
|
||||
solution: {
|
||||
icon: Lightbulb,
|
||||
label: '方案',
|
||||
color: '#C41E3A',
|
||||
bg: '#fef2f2',
|
||||
},
|
||||
service: {
|
||||
icon: Wrench,
|
||||
label: '服务',
|
||||
color: '#7c3aed',
|
||||
bg: '#f5f3ff',
|
||||
},
|
||||
};
|
||||
|
||||
export function CrossRecommendGrid({ items, title = '相关推荐' }: CrossRecommendGridProps) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="bg-white py-16 lg:py-20">
|
||||
<div className="container-wide">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="mb-2 text-center text-2xl font-bold text-gray-900"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500"
|
||||
>
|
||||
探索更多可能,发现与您需求相关的产品、方案和服务
|
||||
</motion.p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((item, index) => {
|
||||
const config = typeConfig[item.type];
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<motion.a
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: index * 0.08 }}
|
||||
className="group flex items-start gap-4 rounded-xl border border-gray-100 bg-gray-50/50 p-5 transition-all hover:border-gray-200 hover:bg-white hover:shadow-md"
|
||||
>
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{ backgroundColor: config.bg, color: config.color }}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span
|
||||
className="rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
style={{
|
||||
color: config.color,
|
||||
backgroundColor: `${config.color}15`,
|
||||
}}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">{item.title}</h3>
|
||||
<p className="mt-1 text-xs leading-relaxed text-gray-500">
|
||||
{item.reason}
|
||||
</p>
|
||||
</div>
|
||||
</motion.a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import type { HeroTheme } from '@/lib/constants/hero-themes';
|
||||
|
||||
interface DetailCTASectionProps {
|
||||
theme: HeroTheme;
|
||||
title?: string;
|
||||
description?: string;
|
||||
primaryAction: { label: string; href: string };
|
||||
secondaryAction?: { label: string; href: string };
|
||||
}
|
||||
|
||||
export function DetailCTASection({
|
||||
theme,
|
||||
title = '准备好开始了吗?',
|
||||
description = '联系我们,获取专属方案和报价',
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
}: DetailCTASectionProps) {
|
||||
return (
|
||||
<section
|
||||
className="py-16 lg:py-20"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${theme.gradientFrom}, ${theme.gradientTo})`,
|
||||
}}
|
||||
>
|
||||
<div className="container-wide text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-white lg:text-3xl">{title}</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-sm text-white/70">{description}</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
href={primaryAction.href}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-white px-8 py-3 text-sm font-semibold text-gray-900 shadow-lg transition-all hover:shadow-xl"
|
||||
>
|
||||
{primaryAction.label}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
{secondaryAction && (
|
||||
<a
|
||||
href={secondaryAction.href}
|
||||
className="inline-flex items-center rounded-lg border border-white/30 px-6 py-3 text-sm font-semibold text-white transition-all hover:bg-white/10"
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import type { DataProof } from '@/lib/constants/products';
|
||||
|
||||
interface DataProofGridProps {
|
||||
proofs: DataProof[];
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export function DataProofGrid({ proofs, accentColor = '#C41E3A' }: DataProofGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
{proofs.map((proof, index) => (
|
||||
<motion.div
|
||||
key={proof.metric}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="rounded-xl border border-gray-200 bg-white p-6 text-center shadow-sm"
|
||||
>
|
||||
<p
|
||||
className="text-3xl font-bold tracking-tight"
|
||||
style={{ color: accentColor }}
|
||||
>
|
||||
{proof.value}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-semibold text-gray-900">{proof.metric}</p>
|
||||
<p className="mt-1 text-xs text-gray-500">{proof.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import type { HeroTheme, HeroTexture } from '@/lib/constants/hero-themes';
|
||||
|
||||
interface DetailHeroProps {
|
||||
theme: HeroTheme;
|
||||
badge?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description?: string;
|
||||
primaryAction?: { label: string; href: string };
|
||||
secondaryAction?: { label: string; href: string };
|
||||
}
|
||||
|
||||
function TextureOverlay({ texture }: { texture: HeroTexture }) {
|
||||
if (texture.type === 'none') return null;
|
||||
|
||||
const textureStyles: Record<string, React.CSSProperties> = {
|
||||
grid: {
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px)',
|
||||
backgroundSize: '60px 60px',
|
||||
},
|
||||
'data-flow': {
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 20% 50%, rgba(255,255,255,.04) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,.03) 0%, transparent 40%)',
|
||||
},
|
||||
layers: {
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, transparent 0%, rgba(255,255,255,.02) 50%, transparent 100%)',
|
||||
},
|
||||
'shield-hex': {
|
||||
backgroundImage:
|
||||
'repeating-linear-gradient(60deg, transparent, transparent 30px, rgba(255,255,255,.025) 30px, rgba(255,255,255,.025) 31px)',
|
||||
},
|
||||
circuit: {
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, transparent 49%, rgba(255,255,255,.02) 49%, rgba(255,255,255,.02) 51%, transparent 51%)',
|
||||
backgroundSize: '40px 40px',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{ opacity: texture.opacity, ...textureStyles[texture.type] }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailHero({
|
||||
theme,
|
||||
badge,
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
}: DetailHeroProps) {
|
||||
const isCenterLayout = theme.layout === 'center' || theme.layout === 'wide';
|
||||
const isIconLeft = theme.layout === 'icon-left';
|
||||
|
||||
const containerClasses =
|
||||
theme.layout === 'wide'
|
||||
? 'container-wide py-24 lg:py-32'
|
||||
: 'container-wide py-20 lg:py-28';
|
||||
|
||||
const contentClasses = [
|
||||
'max-w-4xl',
|
||||
isCenterLayout ? 'mx-auto text-center' : '',
|
||||
isIconLeft ? 'flex items-center gap-8' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative overflow-hidden"
|
||||
style={{
|
||||
background: `linear-gradient(${theme.gradientAngle}deg, ${theme.gradientFrom}, ${theme.gradientTo})`,
|
||||
}}
|
||||
>
|
||||
<TextureOverlay texture={theme.texture} />
|
||||
|
||||
<div className={containerClasses}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
className={contentClasses}
|
||||
>
|
||||
{(badge || theme.badge) && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.1, duration: 0.4 }}
|
||||
className={`mb-4 inline-block rounded-full px-3 py-1 text-xs font-medium tracking-wide ${
|
||||
isCenterLayout ? '' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: theme.accentBg,
|
||||
color: theme.accentColor,
|
||||
border: `1px solid ${theme.accentColor}33`,
|
||||
}}
|
||||
>
|
||||
{badge || theme.badge}
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
<h1
|
||||
className="text-3xl font-bold tracking-tight text-white sm:text-4xl lg:text-5xl"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className={`mt-4 text-lg ${
|
||||
isCenterLayout ? 'mx-auto max-w-2xl' : 'max-w-2xl'
|
||||
}`}
|
||||
style={{ color: `${theme.accentColor}cc`, lineHeight: 1.6 }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
{description && (
|
||||
<p
|
||||
className={`mt-3 text-base leading-relaxed ${
|
||||
isCenterLayout ? 'mx-auto max-w-2xl' : 'max-w-2xl'
|
||||
}`}
|
||||
style={{ color: 'rgba(255,255,255,0.7)' }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(primaryAction || secondaryAction) && (
|
||||
<div
|
||||
className={`mt-8 flex flex-wrap gap-4 ${
|
||||
isCenterLayout ? 'justify-center' : ''
|
||||
}`}
|
||||
>
|
||||
{primaryAction && (
|
||||
<a
|
||||
href={primaryAction.href}
|
||||
className="inline-flex items-center rounded-lg px-6 py-3 text-sm font-semibold text-white shadow-lg transition-all hover:shadow-xl hover:brightness-110"
|
||||
style={{
|
||||
backgroundColor: theme.accentColor,
|
||||
}}
|
||||
>
|
||||
{primaryAction.label}
|
||||
</a>
|
||||
)}
|
||||
{secondaryAction && (
|
||||
<a
|
||||
href={secondaryAction.href}
|
||||
className="inline-flex items-center rounded-lg border px-6 py-3 text-sm font-semibold transition-all hover:bg-white/10"
|
||||
style={{
|
||||
borderColor: `${theme.accentColor}66`,
|
||||
color: `${theme.accentColor}ee`,
|
||||
}}
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { CheckCircle2, Zap, Shield, TrendingUp, type LucideIcon } from 'lucide-react';
|
||||
import type { Product } from '@/lib/constants/products';
|
||||
|
||||
interface ProductValueSectionProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const icons: LucideIcon[] = [CheckCircle2, Zap, Shield, TrendingUp];
|
||||
|
||||
export function ProductValueSection({ product }: ProductValueSectionProps) {
|
||||
return (
|
||||
<section className="bg-white py-16 lg:py-20">
|
||||
<div className="container-wide">
|
||||
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="mb-4 text-2xl font-bold text-gray-900 lg:text-3xl">
|
||||
核心能力
|
||||
</h2>
|
||||
<p className="mb-6 text-sm leading-relaxed text-gray-600">
|
||||
{product.overview}
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{product.features.map((feature, index) => {
|
||||
const Icon = icons[index % icons.length]!;
|
||||
return (
|
||||
<motion.li
|
||||
key={feature}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<Icon className="mt-0.5 h-5 w-5 shrink-0 text-[#C41E3A]" />
|
||||
<span className="text-sm leading-relaxed text-gray-700">{feature}</span>
|
||||
</motion.li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.15 }}
|
||||
>
|
||||
<h2 className="mb-4 text-2xl font-bold text-gray-900 lg:text-3xl">
|
||||
为什么选择我们
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{product.benefits.map((benefit, index) => (
|
||||
<motion.div
|
||||
key={benefit}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.06 }}
|
||||
className="rounded-lg border border-gray-100 bg-gray-50 p-4"
|
||||
>
|
||||
<CheckCircle2 className="mb-2 h-5 w-5 text-green-600" />
|
||||
<p className="text-sm leading-relaxed text-gray-700">{benefit}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-xl bg-gray-900 p-6">
|
||||
<h3 className="mb-3 text-sm font-semibold text-white">技术规格</h3>
|
||||
<ul className="space-y-2">
|
||||
{product.specs.slice(0, 4).map((spec) => (
|
||||
<li key={spec} className="flex items-start gap-2 text-xs text-gray-300">
|
||||
<span className="mt-1 h-1 w-1 shrink-0 rounded-full bg-[#C41E3A]" />
|
||||
{spec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Target,
|
||||
Rocket,
|
||||
ClipboardCheck,
|
||||
Headphones,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import type { Service } from '@/lib/constants/services';
|
||||
|
||||
interface ServiceValueSectionProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const processIcons: LucideIcon[] = [Target, Rocket, ClipboardCheck, Headphones];
|
||||
|
||||
export function ServiceValueSection({ service }: ServiceValueSectionProps) {
|
||||
return (
|
||||
<section className="bg-white py-16 lg:py-20">
|
||||
<div className="container-wide">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-10"
|
||||
>
|
||||
<p className="text-sm leading-relaxed text-gray-600">{service.overview}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="mb-6 text-xl font-bold text-gray-900">服务能力</h2>
|
||||
<ul className="space-y-3">
|
||||
{service.features.map((feature, index) => (
|
||||
<motion.li
|
||||
key={feature}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-[#C41E3A]" />
|
||||
<span className="text-sm leading-relaxed text-gray-700">{feature}</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h3 className="mb-4 mt-8 text-lg font-bold text-gray-900">核心价值</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{service.benefits.map((benefit, index) => (
|
||||
<motion.div
|
||||
key={benefit}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.06 }}
|
||||
className="rounded-lg border border-red-50 bg-red-50/30 p-3"
|
||||
>
|
||||
<p className="text-sm text-gray-700">{benefit}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.15 }}
|
||||
>
|
||||
<h2 className="mb-6 flex items-center gap-2 text-xl font-bold text-gray-900">
|
||||
<Clock className="h-5 w-5 text-[#C41E3A]" />
|
||||
服务流程
|
||||
</h2>
|
||||
<div className="relative">
|
||||
{service.process.map((step, index) => {
|
||||
const Icon = processIcons[index % processIcons.length]!;
|
||||
const isLast = index === service.process.length - 1;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={step}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.08 }}
|
||||
className="relative flex gap-4 pb-6 last:pb-0"
|
||||
>
|
||||
{!isLast && (
|
||||
<div className="absolute left-[19px] top-11 h-full w-px bg-gray-200" />
|
||||
)}
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#C41E3A] text-white shadow-md">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-[#C41E3A]">
|
||||
步骤 {index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{step.split(':')[0]}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs leading-relaxed text-gray-500">
|
||||
{step.split(':')[1] || step.split(':')[1] || ''}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { AlertTriangle, CheckCircle2, ArrowRight, Package, Wrench } from 'lucide-react';
|
||||
import type { Solution } from '@/lib/constants/solutions';
|
||||
import { PRODUCTS } from '@/lib/constants/products';
|
||||
import { SERVICES } from '@/lib/constants/services';
|
||||
|
||||
interface SolutionValueSectionProps {
|
||||
solution: Solution;
|
||||
}
|
||||
|
||||
export function SolutionValueSection({ solution }: SolutionValueSectionProps) {
|
||||
const primaryProducts = solution.suiteCombination.primaryProducts
|
||||
.map((id) => PRODUCTS.find((p) => p.id === id))
|
||||
.filter(Boolean);
|
||||
|
||||
const complementaryServices = solution.suiteCombination.complementaryServices
|
||||
.map((id) => SERVICES.find((s) => s.id === id))
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<section className="bg-white py-16 lg:py-20">
|
||||
<div className="container-wide">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-12 text-center"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-gray-900 lg:text-3xl">
|
||||
{solution.valueProposition.headline}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="mb-12 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{solution.valueProposition.points.map((point, index) => (
|
||||
<motion.div
|
||||
key={point.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
className="rounded-xl border border-gray-100 bg-gray-50/50 p-6 text-center"
|
||||
>
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-[#C41E3A]/10 text-[#C41E3A]">
|
||||
<span className="text-lg">{point.icon}</span>
|
||||
</div>
|
||||
<h3 className="mb-2 text-base font-semibold text-gray-900">{point.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-600">{point.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-gray-900">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500" />
|
||||
行业挑战
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{solution.challenges.map((challenge, index) => (
|
||||
<motion.li
|
||||
key={challenge}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="flex items-start gap-2 text-sm text-gray-700"
|
||||
>
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-orange-400" />
|
||||
{challenge}
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.15 }}
|
||||
>
|
||||
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-gray-900">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
解决方案
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{solution.solutions.map((sol, index) => (
|
||||
<motion.li
|
||||
key={sol}
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
className="flex items-start gap-2 text-sm text-gray-700"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||
{sol}
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{(primaryProducts.length > 0 || complementaryServices.length > 0) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="mt-12 rounded-2xl bg-gradient-to-br from-gray-50 to-gray-100 p-8"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-bold text-gray-900">推荐套装组合</h3>
|
||||
<p className="mb-6 text-sm leading-relaxed text-gray-600">
|
||||
{solution.suiteCombination.rationale}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{primaryProducts.map((product) =>
|
||||
product ? (
|
||||
<a
|
||||
key={product.id}
|
||||
href={`/products/${product.id}`}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-white px-4 py-2 text-sm font-medium text-blue-700 transition-all hover:bg-blue-50 hover:border-blue-300"
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
{product.title}
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</a>
|
||||
) : null,
|
||||
)}
|
||||
{complementaryServices.map((service) =>
|
||||
service ? (
|
||||
<a
|
||||
key={service.id}
|
||||
href={`/services/${service.id}`}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-purple-200 bg-white px-4 py-2 text-sm font-medium text-purple-700 transition-all hover:bg-purple-50 hover:border-purple-300"
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
{service.title}
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</a>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { CaseStudyCard } from './detail-case-study';
|
||||
import { DataProofGrid } from './detail-data-proof';
|
||||
import { CertificationList } from './detail-certification';
|
||||
import type { CaseStudy, DataProof, Certification } from '@/lib/constants/products';
|
||||
|
||||
interface DetailTrustSectionProps {
|
||||
caseStudies?: CaseStudy[];
|
||||
dataProofs?: DataProof[];
|
||||
certifications?: Certification[];
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export function DetailTrustSection({
|
||||
caseStudies = [],
|
||||
dataProofs = [],
|
||||
certifications = [],
|
||||
accentColor = '#C41E3A',
|
||||
}: DetailTrustSectionProps) {
|
||||
const hasContent = caseStudies.length > 0 || dataProofs.length > 0 || certifications.length > 0;
|
||||
if (!hasContent) return null;
|
||||
|
||||
return (
|
||||
<section className="bg-gray-50 py-16 lg:py-20">
|
||||
<div className="container-wide">
|
||||
{dataProofs.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
|
||||
关键数据
|
||||
</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500">
|
||||
用真实数据说话,让效果可衡量、可验证
|
||||
</p>
|
||||
<DataProofGrid proofs={dataProofs} accentColor={accentColor} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{caseStudies.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
|
||||
客户案例
|
||||
</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500">
|
||||
来自真实客户的成功实践,看看他们如何实现转型突破
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{caseStudies.map((study, index) => (
|
||||
<CaseStudyCard key={study.client} study={study} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{certifications.length > 0 && (
|
||||
<div>
|
||||
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
|
||||
资质认证
|
||||
</h2>
|
||||
<p className="mx-auto mb-6 max-w-2xl text-center text-sm text-gray-500">
|
||||
通过权威机构认证,保障安全合规
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<CertificationList certifications={certifications} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user