问题:Tailwind 原生 bg-white 是硬编码值,深色主题下不翻转, 导致整页/整 section 恒为纯白,与转深色的页头页脚割裂。 (globals.css:--color-bg-primary-rgb 浅色 255 255 255 → 深色 10 14 20) 改动:10 文件 67 处 bg-white → 项目 token,按语义二分类 - bg-bg-primary (33 处):页面/大块骨架(min-h-screen 容器、<section>、<main>) - bg-bg-elevated (34 处):浮起层(带 border 的卡片、表单输入、图标盒、grid 单元格) --color-bg-elevated 浅色 #FFFFFF → 深色 #151B23,同样翻转。 浅色主题下 bg-white / bg-bg-primary / bg-bg-elevated 三者同值(纯白), 故本次改动在浅色主题下零像素差异,只修复深色主题。 新增审计工具: - scripts/audit/hardcoded-color-audit.mjs:硬编码颜色分级盘点 (含 alpha 误报修正:bg-white/NN 半透明叠加两主题均成立,判 INFO 非 P0) - scripts/audit/darkmode-bg-verify.mjs:直接验证背景随主题翻转 (浅色应纯白、深色应 rgb(10,14,20) 且无残留纯白块) 审计结果:marketing P0 67 → 0;全站 P0 97 → 30 (剩余 components 16 / other 8 / layout 3 / ui-kit 3 为第二批;admin 本就为 0) 门禁: - tsc 0 error - eslint 0 error(50 warnings 全为既有,改动文件未新增) - jest 129/130 套件通过,1548 通过 / 2 跳过 / **0 断言失败** 唯一失败 src/lib/admin-api.test.ts 为**沙箱环境限制非代码问题**: CODEBUDDY_BROKER_DENY 拦截 readFileSync,单独复验同样失败, 与本次改动零交集(改动仅限 src/app/(marketing)/ 下 10 个页面组件) - 暗黑模式背景验证 18/18 PASS(9 路由 × 浅深双版)
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 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">大</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">端</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">化</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">{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 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>
|
|
);
|
|
}
|