Files
novalon-website/src/app/(marketing)/solutions/solution-detail-content-v3.tsx
T
zhangxiang 846585af1f refactor(theme): 营销页 bg-white token 化(暗黑模式 Phase 2 · marketing 批次)
问题: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 路由 × 浅深双版)
2026-09-04 13:46:29 +08:00

406 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { CTAButton } from '@/components/ui/cta-button';
import { Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, Package, BookOpen, MessageSquare, Calendar, Smartphone, Route, Shield, Truck } from 'lucide-react';import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Solution } from '@/lib/constants/solutions';
import type { Product } from '@/lib/constants/products';
import { L3EmptyFallback, CaseStudiesSection, CertificationsSection } from '@/components/detail';
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
制造业: <Factory className="w-8 h-8" />,
零售业: <ShoppingCart className="w-8 h-8" />,
医疗: <Heart className="w-8 h-8" />,
教育: <GraduationCap className="w-8 h-8" />,
};
const VALUE_ICONS: Record<string, React.ReactNode> = {
Factory: <Factory className="w-6 h-6" />,
Package: <Package className="w-6 h-6" />,
TrendingUp: <TrendingUp className="w-6 h-6" />,
Users: <Users className="w-6 h-6" />,
ShoppingCart: <ShoppingCart className="w-6 h-6" />,
GraduationCap: <GraduationCap className="w-6 h-6" />,
BookOpen: <BookOpen className="w-6 h-6" />,
MessageSquare: <MessageSquare className="w-6 h-6" />,
Heart: <Heart className="w-6 h-6" />,
Calendar: <Calendar className="w-6 h-6" />,
Smartphone: <Smartphone className="w-6 h-6" />,
Route: <Route className="w-6 h-6" />,
Shield: <Shield className="w-6 h-6" />,
Truck: <Truck className="w-6 h-6" />,
};
interface DetailHeroProps {
solution: Solution;
}
function DetailHero({ solution }: DetailHeroProps) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-bg-primary pt-24">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-20 sm:py-24 md:py-28 lg:py-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{industryIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{solution.industry}
</span>
<div className="text-text-muted text-sm mt-2">{solution.subtitle}</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{solution.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-12"
>
{solution.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.45, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<CTAButton href="/contact" dataTestId="solution-detail-hero-primary">
获取方案详情
</CTAButton>
<CTAButton href="/products" variant="secondary" dataTestId="solution-detail-hero-secondary">
查看产品
</CTAButton>
</motion.div>
</div>
</section>
);
}
interface ChallengesSectionProps {
challenges: string[];
}
function ChallengesSection({ challenges }: ChallengesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 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">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Challenges</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
行业痛点
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
我们深入理解行业面临的核心挑战,提供针对性的解决方案
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.05} delayChildren={0.05}>
{challenges.map((challenge, idx) => (
<div
key={idx}
className="group relative p-8 bg-bg-elevated hover:bg-bg-secondary transition-all duration-150"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-150 origin-top" />
<div className="flex items-start gap-6">
<div className="w-10 h-10 rounded-xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0 mt-0.5">
<span className="text-lg font-bold">{idx + 1}</span>
</div>
<p className="text-lg text-ink leading-relaxed">{challenge}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface SolutionsSectionProps {
solutions: string[];
}
function SolutionsSection({ solutions }: SolutionsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Solutions
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
解决方案
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.05} delayChildren={0.05}>
{solutions.map((solution, idx) => (
<div
key={idx}
className="group relative p-10 bg-bg-elevated hover:bg-bg-secondary transition-all duration-150"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-150 origin-left" />
<div className="flex items-start gap-6">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
<Lightbulb className="w-7 h-7" />
</div>
<div>
<div className="text-xs font-bold text-brand mb-3 tracking-widest uppercase">
方案 {idx + 1}
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{solution}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ValuePropositionProps {
valueProposition: Solution['valueProposition'];
}
function ValuePropositionSection({ valueProposition }: ValuePropositionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 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">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Value Proposition
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
{valueProposition.headline}
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.05} delayChildren={0.05}>
{valueProposition.points.map((point, idx) => {
const icon = VALUE_ICONS[point.icon] || <Settings className="w-6 h-6" />;
return (
<div
key={idx}
className="group relative p-10 text-center bg-bg-elevated hover:bg-bg-secondary transition-all duration-150"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-150 origin-center" />
<div className="w-20 h-20 rounded-3xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-8">
{icon}
</div>
<h3 className="text-2xl font-bold text-ink mb-4">{point.title}</h3>
<p className="text-text-secondary leading-relaxed">{point.description}</p>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface SuiteCombinationProps {
suiteCombination: Solution['suiteCombination'];
products: Product[];
}
function SuiteCombinationSection({ suiteCombination, products }: SuiteCombinationProps) {
const primaryProducts = suiteCombination.primaryProducts
.map(pid => products.find(p => p.id === pid))
.filter(Boolean);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Suite Combination
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
推荐产品组合
</h2>
</ScrollReveal>
<div className="grid lg:grid-cols-5 gap-px bg-border-primary mb-px">
<StaggerReveal staggerDelay={0.05} delayChildren={0.05}>
{primaryProducts.map((product) => product && (
<div
key={product.id}
className="group relative p-8 bg-bg-elevated hover:bg-bg-secondary transition-all duration-150 text-center lg:col-span-2"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-150 origin-left" />
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Package className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
核心产品
</div>
<h3 className="text-xl font-bold text-ink mb-3">{product.title}</h3>
<p className="text-sm text-text-muted leading-relaxed">{product.description}</p>
</div>
))}
</StaggerReveal>
<ScrollReveal delay={0.2} className="flex items-center justify-center lg:col-span-1 bg-bg-elevated">
<div className="w-20 h-20 rounded-full bg-brand-soft/30 border-2 border-brand/30 flex items-center justify-center text-brand font-bold text-xl">
+
</div>
</ScrollReveal>
<ScrollReveal delay={0.3} className="lg:col-span-2">
<div className="group relative p-8 bg-brand-soft/10 hover:bg-bg-secondary transition-all duration-150 text-center h-full flex flex-col justify-center">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Users className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
配套服务
</div>
<h3 className="text-xl font-bold text-ink mb-3">专业实施服务</h3>
<p className="text-sm text-text-secondary leading-relaxed">
{suiteCombination.complementaryServices.length}项服务配套,确保方案落地见效
</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<div className="p-10 bg-bg-secondary text-center border border-border-primary">
<div className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand mb-4">
方案价值
</div>
<p className="text-xl lg:text-2xl text-ink leading-relaxed max-w-4xl mx-auto font-medium">
{suiteCombination.rationale}
</p>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface CTASectionProps {
solution: Solution;
}
// ===== L3 信任层:客户案例 / 资质认证已合并至共享组件(@/components/detail)=====
// 原本地 CaseStudiesSection / CertificationsSection 与 product 页逐字重复 ~95%,
// B-2(commit 见 git log)统一为共享组件;heading 由 prop 传入以保留原文案。
function CTASection({ solution }: CTASectionProps) {
return (
<section className="relative 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-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
开启您的<br />
<span className="text-brand">数字化转型之旅</span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
联系我们,获取{solution.title}的详细方案和专属报价
</p>
<div className="flex flex-wrap justify-center gap-6">
<CTAButton href="/contact" dataTestId="solution-detail-bottom-primary">
预约咨询
</CTAButton>
<CTAButton href="/solutions" variant="secondary" dataTestId="solution-detail-bottom-secondary">
查看更多方案
</CTAButton>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface SolutionDetailContentV3Props {
solution: Solution;
products?: Product[];
}
export default function SolutionDetailContentV3({ solution, products = [] }: SolutionDetailContentV3Props) {
return (
<main>
<DetailHero solution={solution} />
<ChallengesSection challenges={solution.challenges} />
<SolutionsSection solutions={solution.solutions} />
<ValuePropositionSection valueProposition={solution.valueProposition} />
<SuiteCombinationSection suiteCombination={solution.suiteCombination} products={products} />
<L3EmptyFallback caseStudies={solution.caseStudies} certifications={solution.certifications} />
<CaseStudiesSection caseStudies={solution.caseStudies} heading="同行业落地案例" />
<CertificationsSection certifications={solution.certifications} />
<CTASection solution={solution} />
</main>
);
}