- 拆分品牌色「文字/底色」双通道:新增 --color-brand-ink(-rgb)(深色 #F87171) 与 tailwind brand.ink;text-brand → text-brand-ink 迁移 247 处; bg-brand/border-brand 保持 --color-brand-rgb 不翻转(红底白字 5.58:1 不能动) - dark 块补齐 --color-brand-bg: #2A1418(修复深色白字压浅粉底 1.04:1) - hero 徽章新增 --badge-accent-text 桥接:accentColor 直用作深底文字不可读 (#1e3a5f 深底 1.65:1),浅色取原色 / 深色 color-mix 提亮 - 硬编码颜色 token 化:bg-white/text-gray-*/border-gray-* → bg-bg-*/text-text-*/border-border-* - 可访问性:触控目标 <24px 归零(footer 链接列 [&_a]:min-h-6 收口 377 处等)、 news 搜索框补 aria-label、标题跳级 h2→h4 修正为 h3、装饰 blur 光斑容器补 overflow-hidden 消除伪文本裁剪 - 验证:tsc 0 / eslint 0 error / jest 0 失败用例 / dogfood 33 路由 × 双主题 P1-P3 全零
261 lines
12 KiB
TypeScript
261 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ArrowUpRight, Factory, ShoppingCart, Heart, GraduationCap, Shield, Truck, type LucideIcon } from 'lucide-react';
|
|
import { StaticLink } from '@/components/ui/static-link';
|
|
import { CTAButton } from '@/components/ui/cta-button';
|
|
import { type Solution } from '@/lib/constants/solutions';
|
|
import type { Product } from '@/lib/constants/products';
|
|
|
|
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
|
|
|
|
type PageCopy = Record<string, unknown>;
|
|
|
|
const INDUSTRY_ICONS: Record<string, LucideIcon> = {
|
|
制造业: Factory,
|
|
贸易零售: ShoppingCart,
|
|
医疗健康: Heart,
|
|
教育培训: GraduationCap,
|
|
金融服务: Shield,
|
|
物流运输: Truck,
|
|
};
|
|
|
|
function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
|
const heroTitle1 = (pageCopy?.solutionsHeroTitle1 as string) || '专注行业场景的';
|
|
const heroTitle2 = (pageCopy?.solutionsHeroTitle2 as string) || '解决方案';
|
|
const heroDescriptionLines = ((pageCopy?.solutionsHeroDescription as string) ||
|
|
'端到端交付可量化的业务成果。\n基于自研产品矩阵,为制造、零售、教育、医疗、金融、物流六大行业沉淀最佳实践。').split('\n');
|
|
const ctaPrimary = (pageCopy?.solutionsCtaPrimary as string) || '立即咨询';
|
|
const ctaSecondary = (pageCopy?.solutionsCtaSecondary as string) || '浏览产品';
|
|
|
|
return (
|
|
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-bg-primary">
|
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
|
<div className="max-w-4xl">
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 40 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: 0.2, ease: EASE_OUT }}
|
|
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
|
>
|
|
<div className="block">{heroTitle1}</div>
|
|
<div className="block mt-4 sm:mt-6">
|
|
<span className="relative inline-block">
|
|
<span className="relative text-brand-ink font-black">
|
|
{heroTitle2}
|
|
<motion.span
|
|
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
|
style={{ transformOrigin: 'left' }}
|
|
initial={{ scaleX: 0 }}
|
|
animate={{ scaleX: 1 }}
|
|
transition={{ duration: 0.3, delay: 1.2, ease: EASE_OUT }}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</motion.h1>
|
|
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: 0.5, ease: EASE_OUT }}
|
|
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
|
|
>
|
|
{heroDescriptionLines.map((line, i) => (
|
|
<span key={i}>
|
|
{i > 0 && <br className="hidden sm:block" />}
|
|
{line}
|
|
</span>
|
|
))}
|
|
</motion.p>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: 0.7, ease: EASE_OUT }}
|
|
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
|
|
>
|
|
<CTAButton href="/contact" dataTestId="solutions-hero-primary" className="min-h-[52px]">
|
|
{ctaPrimary}
|
|
</CTAButton>
|
|
<CTAButton href="/products" variant="secondary" dataTestId="solutions-hero-secondary" className="min-h-[52px]">
|
|
{ctaSecondary}
|
|
</CTAButton>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: 0.9, ease: EASE_OUT }}
|
|
className="grid grid-cols-3 gap-8 mt-20 pt-12 border-t border-border-primary max-w-xl"
|
|
>
|
|
<div>
|
|
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
|
|
6<span className="text-brand-ink">大</span>
|
|
</div>
|
|
<div className="text-sm text-text-muted">行业覆盖</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
|
|
端到<span className="text-brand-ink">端</span>
|
|
</div>
|
|
<div className="text-sm text-text-muted">交付模式</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
|
|
定制<span className="text-brand-ink">化</span>
|
|
</div>
|
|
<div className="text-sm text-text-muted">方案特点</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function SolutionCard({ solution, index, products, featured = false }: { solution: Solution; index: number; products?: Product[]; featured?: boolean }) {
|
|
const productLookup = products ?? [];
|
|
const Icon = INDUSTRY_ICONS[solution.industry] || Factory;
|
|
const recommendedProductNames = (solution.recommendedProducts ?? [])
|
|
.map((pid) => productLookup.find((p) => p.id === pid)?.title)
|
|
.filter(Boolean) as string[];
|
|
|
|
return (
|
|
<motion.div
|
|
data-testid={`solution-card-${solution.id}`}
|
|
data-featured={featured ? 'true' : 'false'}
|
|
className={featured ? 'md:col-span-2 bg-bg-elevated' : 'md:col-span-1 bg-bg-elevated'}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-60px' }}
|
|
transition={{ duration: 0.3, delay: index * 0.06, ease: EASE_OUT }}
|
|
>
|
|
<StaticLink
|
|
href={`/solutions/${solution.id}`}
|
|
className="group relative block transition-all duration-300 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-bg-elevated border border-border-primary hover:border-brand/20 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-brand after:transition-transform after:duration-300 group-hover:after:scale-x-100"
|
|
>
|
|
<div className="relative z-10 p-8 sm:p-10 md:p-12">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Icon className="w-5 h-5 text-text-muted" strokeWidth={1.5} />
|
|
<h3 className="text-lg font-bold text-text-primary">
|
|
{solution.industry}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<div className="text-xs text-text-muted mb-2">核心痛点</div>
|
|
<p className="text-sm text-text-secondary leading-relaxed">
|
|
{solution.painPoints?.join('、')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 mb-4">
|
|
{solution.outcomes?.map((o) => (
|
|
<div key={o.label} className="p-2 bg-bg-secondary">
|
|
<div className="text-base font-bold text-brand-ink">{o.value}</div>
|
|
<div className="text-xs text-text-muted">{o.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-border-primary">
|
|
<div className="text-xs text-text-muted mb-2">推荐产品组合</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{recommendedProductNames.map((name) => (
|
|
<span key={name} className="px-2 py-1 text-xs bg-bg-secondary text-text-secondary">
|
|
{name.replace('睿新', '')}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors duration-300 mt-6">
|
|
<span>了解详情</span>
|
|
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
|
|
</div>
|
|
</div>
|
|
</StaticLink>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function SolutionsGridSection({ solutions, products, pageCopy }: { solutions: Solution[]; products?: Product[]; pageCopy?: PageCopy | null }) {
|
|
const gridTitle = (pageCopy?.solutionsGridTitle as string) || '行业解决方案';
|
|
const gridDescription = (pageCopy?.solutionsGridDescription as string) || '每个方案都明确推荐产品组合和服务包,确保落地效果';
|
|
return (
|
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
|
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
|
|
|
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-80px' }}
|
|
transition={{ duration: 0.3, ease: EASE_OUT }}
|
|
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
|
|
>
|
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
|
{gridTitle}
|
|
</h2>
|
|
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
|
{gridDescription}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
|
|
{solutions.map((solution, index) => (
|
|
<SolutionCard key={solution.id} solution={solution} index={index} products={products} featured={index === 0} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
|
const ctaTitle = (pageCopy?.solutionsCtaTitle as string) || '告诉我们您的行业场景';
|
|
const ctaDescription = (pageCopy?.solutionsCtaDescription as string) || '无论您处于哪个行业、哪个数字化阶段,我们都能为您推荐最合适的产品组合和服务包。';
|
|
return (
|
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-primary">
|
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-80px' }}
|
|
transition={{ duration: 0.3, ease: EASE_OUT }}
|
|
className="max-w-3xl"
|
|
>
|
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
|
|
{ctaTitle}
|
|
</h2>
|
|
<p className="text-lg text-text-secondary leading-relaxed mb-10">
|
|
{ctaDescription}
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
|
<CTAButton href="/contact" dataTestId="solutions-bottom-primary" className="min-h-[52px]">
|
|
获取定制方案
|
|
</CTAButton>
|
|
<CTAButton href="/products" variant="secondary" dataTestId="solutions-bottom-secondary" className="min-h-[52px]">
|
|
浏览产品
|
|
</CTAButton>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default function SolutionsContentV3({ solutions: solutionsProp, products, pageCopy }: { solutions?: Solution[]; products?: Product[]; pageCopy?: PageCopy | null }) {
|
|
const solutions = solutionsProp ?? [];
|
|
|
|
return (
|
|
<main className="bg-bg-primary text-ink">
|
|
<HeroSection pageCopy={pageCopy} />
|
|
<SolutionsGridSection solutions={solutions} products={products} pageCopy={pageCopy} />
|
|
<CTASection pageCopy={pageCopy} />
|
|
</main>
|
|
);
|
|
}
|