Files
novalon-website/src/app/(marketing)/home-content-v15.tsx
T
zhangxiang a366bd1400 fix(qa): 第五轮系统性质量保障缺陷修复 + 单元/集成测试
安全/鉴权:登出清服务端 httpOnly 令牌、登录 IP 限流前置 bcrypt、停用账号
令牌在 6 个内联 role handler 失效、admin/items 存在性 oracle 认证前置、
同意偏好 sanitize fail-closed、api-crypto 按 HTTP 方法判体。

CMS:workflow 状态更新+审计收进交互式 $transaction(失败即回滚)、编辑器
richtext 走 textarea 防换行损毁、array/object 子字段按作用域读写防污染、
about/contact/erp-upgrade 补 ISR revalidate 与路由映射。

UI/a11y:AnimatedCounter 入视口前显起点、CLS 累计上报、MetricCard 动画入
effect 并卸载取消 rAF、后台状态四态映射/搜索防抖+序号守卫/媒体单 input/
吞错改横幅/表单 label-aria 关联。

新增对应判别性单测与 pagination/rate-limit/client-ip/sanitize/validate-content-data
等模块及 tests-integration 真库集成层。
2026-09-28 10:48:07 +08:00

995 lines
48 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 { useRef } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import { StaticLink } from '@/components/ui/static-link';
import { HeroParticleField, type HeroVariant } from '@/components/sections/hero-particle-field';
import { ArrowRight, Lightbulb, Database, Layers, Code, Puzzle, Server, Quote, Calendar } from 'lucide-react';
import { cn } from '@/lib/utils';
import { EASE_OUT } from '@/components/ui/page-decoration';
import { CTAButton } from '@/components/ui/cta-button';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from '@/lib/constants';
import { canonicalCtaLabel, CTA_LABELS } from '@/lib/constants/cta-labels';
import { canonicalTrustSource } from '@/lib/constants/company';
import { weakestBasis } from '@/lib/constants/metrics-basis';
import { useSiteConfig } from '@/lib/site-config';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { type NewsItem } from '@/lib/constants/news';
import { NEWS } from '@/lib/constants/news';
// ===== 服务图标映射:将字符串键名映射为 Lucide 组件 =====
const SERVICE_ICON_MAP: Record<string, React.ReactNode> = {
Lightbulb: <Lightbulb className="w-7 h-7" />,
Code: <Code className="w-7 h-7" />,
Server: <Server className="w-7 h-7" />,
Puzzle: <Puzzle className="w-7 h-7" />,
};
// ===== 服务数据降级兜底(4 条,保证 sm:grid-cols-2 网格完整,避免孤儿格)=====
const FALLBACK_SERVICES = [
{
subtitle: 'Strategy Consulting',
title: '数字化转型战略咨询',
desc: '从业务诊断到数字化路线图,帮助企业识别高价值场景,规划可落地的转型路径,确保每一分投入都能产生可衡量的业务回报。',
highlights: ['业务诊断', '数字化蓝图', '实施路线图', 'ROI 评估'],
metrics: [
{ value: '40%', label: '平均运营效率提升' },
{ value: '6个月', label: '战略到试点周期' },
],
href: '/services/consulting',
icon: <Lightbulb className="w-7 h-7" />,
},
{
subtitle: 'Software Delivery',
title: '企业软件定制开发',
desc: '基于自研产品矩阵与工程化交付流程,提供从需求分析、架构设计到持续运维的全栈软件开发服务,支撑业务数字化落地。',
highlights: ['需求分析', '架构设计', '敏捷开发', '持续运维'],
metrics: [
{ value: '6', label: '款自研产品' },
{ value: '24/7', label: '运维支持' },
],
href: '/services/software',
icon: <Code className="w-7 h-7" />,
},
{
subtitle: 'Data Intelligence',
title: '数据智能与 AI 应用',
desc: '构建企业级数据平台与 AI 能力,打通数据孤岛,沉淀数据资产,让决策从经验驱动转向数据驱动。',
highlights: ['数据中台', 'AI 建模', '可视化分析', '预测决策'],
metrics: [
{ value: '98%', label: '数据质量达标率' },
{ value: '10x', label: '分析效率提升' },
],
href: '/services/data',
icon: <Database className="w-7 h-7" />,
},
{
subtitle: 'Solution Delivery',
title: '行业解决方案落地',
desc: '基于核心团队的行业实践,提供从方案设计、系统开发到上线运维的一站式交付,确保数字化转型真正落地生根。',
highlights: ['行业套件', '定制开发', '系统集成', '持续运维'],
metrics: [
{ value: '6', label: '行业场景覆盖' },
{ value: '99.9%', label: '系统可用性' },
],
href: '/services/solutions',
icon: <Layers className="w-7 h-7" />,
},
];
// ===== 行业洞察兜底数据(对标 Accenture Insights:类型标签 + 卡片)=====
// 零编造原则:全部为可验证的方法论/观点类内容,链接真实页面;不做虚构研究报告
const FALLBACK_INSIGHTS = [
{
tag: '方法论',
title: '数字化转型诊断:从哪里开始,如何衡量',
desc: '从业务目标出发的四步诊断法——梳理断点、量化差距、排定优先级、定义验收指标。',
date: '2026-08',
href: '/methodology',
},
{
tag: '观点',
title: '私有化部署:为什么核心数据需要留在企业内',
desc: '合规要求之外,数据主权正在成为企业数字化决策的第一优先级。',
date: '2026-08',
href: '/services/data',
},
{
tag: '方法论',
title: '从需求到交付:全流程陪伴式实施如何降低转型风险',
desc: '诊断 → 设计 → 交付 → 运维,每一阶段都有明确的交付物与衡量指标。',
date: '2026-07',
href: '/services/software',
},
];
// ===== 创始人观点兜底(对标 Accenture CEO Quote,人格化背书)=====
// 零编造原则:不虚构具体人名/原话;姓名职务为空时仅展示观点立场,真实引言可在 CMS page-copy 配置
const FALLBACK_FOUNDER_QUOTE = {
// 排版约束:3 行均势(17 / 18 / 13 字),避开"量。"孤儿字。
// 配合 blockquote 的 whitespace-pre-line 渲染换行。
text: '我们相信,数字化不是一次性项目,\n而是持续迭代的能力建设。\n每一份投入,都应当经得起衡量。',
name: '睿新致远创始团队',
role: '技术咨询 · 数字化转型同行者',
};
// ===== 首页结构性文案类型(page-copy CMS 覆盖,未配置时走硬编码兜底)=====
type HomePageCopy = Record<string, any>;
// 多行标题渲染:按 \n 分段,输出 <br/> 换行
function StyledTitle({ lines }: { lines: (string | undefined)[] }) {
return (
<>
{lines.map((line, i) => (
<span key={i}>
{i > 0 && <br />}
{line}
</span>
))}
</>
);
}
// ===== 品牌 CTA 按钮已抽离为可复用 UI 组件:@/components/ui/cta-button =====
// ===== Hero 区:浅色画布 + 精密工程网格科技感 + 品牌红单电压 =====
// 设计方向(2026-08-31 重设计,参考 Accenture Radical Relevance;2026-09-01 科技感升级):
// - 声明先行:H1 → 主张 → CTA,一屏只负责「一句话说清你是谁 + 一个行动」
// - 数据指标下沉为独立成果带(HeroStatsBand),由滚动触发(Glance 层),不再挤在 hero 内
// - 单一电压:hero 内只有品牌红一个彩色;连线走墨色(浅底)/中性灰(深底),不抢品牌红
// - 字重收敛:大标题 700(原 900),对齐 Accenture「字重 9→3」的克制
// - 科技感引擎:HeroParticleField 变体(固定种子、reduced-motion/自动化降级 → 快照零漂移)
//
// ===== HERO_VARIANT 观感开关(默认 'grid':浅色底 + 工程网格科技感)=====
// 定调:Hero 保持浅色背景(与全站浅色咨询风一致),科技感由「精密工程网格 + 动态粒子 + 墨色细连线」承担,
// 不做深色 Hero。深色变体保留待选(改这一行即生效,无需改其它代码):
// 'grid' 浅色工程网格 + 品牌红漂移节点 + 墨色细连线 ← 默认落地
// 'network' 深色节点网络(品牌红节点 + 中性灰连线)
// 'constellation' 深色数据星图(品牌红脉冲节点 + 暗白次级点 + 竖向数据流)
// 'spotlight' 深色聚光 + 粒子 + 工程网格(纵深最强,复用 L2 光标聚光)
const HERO_VARIANT: HeroVariant = 'grid';
function HeroSection({ heroData }: {
heroData?: Record<string, any> | null;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const spotRef = useRef<HTMLDivElement>(null);
const siteConfig = useSiteConfig();
const reduceMotion = useReducedMotion();
// 科技感变体开关(见文件顶部 HERO_VARIANT 注释):深色变体统一走深色画布 + 主题感知文字
const dark = HERO_VARIANT !== 'grid';
const showGrid = HERO_VARIANT === 'constellation' || HERO_VARIANT === 'spotlight'; // 深色工程网格
const showLightGrid = HERO_VARIANT === 'grid'; // 浅色工程网格(墨色 hairline,精密质感)
const showCursorGlow = HERO_VARIANT === 'spotlight';
const heading = heroData?.heading || heroData?.headingBottom || '让每一家企业都拥有数据驱动的决策能力';
const subheading = heroData?.subheading || heroData?.description || '';
const ctaLabel = canonicalCtaLabel(heroData?.ctaLabel) || CTA_LABELS.consult;
const ctaHref = heroData?.ctaHref || '/contact';
const secondaryCtaLabel = heroData?.secondaryCtaLabel || '查看产品矩阵';
const secondaryCtaHref = heroData?.secondaryCtaHref || '/products';
const slogan = siteConfig.slogan || COMPANY_INFO.slogan;
// reduced-motion 守卫:与 ScrollReveal 同范式——reduce 时传空 initial/animate,
// 元素直接以自然(可见)状态渲染,零位移/淡入;默认(非 reduce,含首帧 null)正常动画。
const rise = (y: number, delay: number, duration = 0.3) => ({
initial: reduceMotion ? {} : { opacity: 0, y },
animate: reduceMotion ? {} : { opacity: 1, y: 0 },
transition: { duration: reduceMotion ? 0 : duration, delay: reduceMotion ? 0 : delay, ease: EASE_OUT },
});
return (
<section
ref={containerRef}
className={`relative min-h-[90vh] flex items-center overflow-hidden ${dark ? 'bg-dark-bg text-white' : 'bg-bg-primary'}`}
data-testid="hero-section"
onPointerMove={(e) => {
if (!spotRef.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
spotRef.current.style.setProperty('--spot-x', `${e.clientX - rect.left}px`);
spotRef.current.style.setProperty('--spot-y', `${e.clientY - rect.top}px`);
}}
>
{/* L1 工程网格:浅底墨色 hairline(grid)/ 深底白色 hairline(constellation、spotlight) */}
{showLightGrid && (
<div className="absolute inset-0 pointer-events-none hero-grid-overlay--light" aria-hidden="true" />
)}
{showGrid && (
<div className="absolute inset-0 pointer-events-none hero-grid-overlay--dark" aria-hidden="true" />
)}
{/* L2 品牌红光标聚光(仅 spotlight 变体 + 非 reduced-motion):跟随鼠标,仅写 2 个 CSS 变量,无 canvas、无逐帧 JS */}
{showCursorGlow && !reduceMotion && (
<div ref={spotRef} className="absolute inset-0 pointer-events-none hero-spot-glow opacity-70" aria-hidden="true" />
)}
{/* L3 动态粒子(品牌红点缀 + 科技感变体):rAF 持续循环漂移;
reduced-motion / 自动化环境下降级为一次性静态帧(连线/星图位置由固定种子复现 → 快照零漂移) */}
<HeroParticleField variant={HERO_VARIANT} dark={dark} />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-16 sm:py-20 md:py-24 lg:py-24">
<div className="grid items-center gap-16">
<div className="max-w-4xl">
{/* 品牌口号 Badge:画布上的品牌红信号(深底走 brand-light 保证对比度;hero 不重复渲染 Logo,避免与导航栏重复) */}
{slogan && (
<motion.div
{...rise(20, 0.03)}
className="mb-6"
>
<span className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium border ${dark ? 'bg-brand/10 text-brand-light border-brand/30' : 'bg-brand/5 text-brand-ink border-brand/15'}`}>
{slogan}
</span>
</motion.div>
)}
{/* 主标题:大胆、直接(深底白色 / 浅底墨色,随 HERO_VARIANT 切换) */}
<motion.h1
{...rise(40, 0.06, 0.45)}
className={`text-4xl sm:text-5xl md:text-6xl lg:text-6xl 2xl:text-7xl font-bold leading-[1.05] tracking-tight mb-7 sm:mb-8 ${dark ? 'text-white' : 'text-ink'}`}
>
{heading}
</motion.h1>
{/* 主张:紧随声明之后,回答「凭什么信你」(声明先行,数据下沉至成果带) */}
{subheading && (
<motion.p
{...rise(30, 0.16)}
className={`text-lg sm:text-xl max-w-2xl leading-relaxed mb-10 ${dark ? 'text-white/70' : 'text-text-secondary'}`}
>
{subheading}
</motion.p>
)}
{/* CTA 按钮组 */}
<motion.div
{...rise(30, 0.24)}
className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6"
>
<CTAButton href={ctaHref} dataTestId="hero-primary-cta">{ctaLabel}</CTAButton>
{secondaryCtaLabel && (
<a
href={secondaryCtaHref}
data-testid="hero-secondary-link"
className={`group inline-flex items-center gap-2 text-sm font-medium transition-colors duration-200 py-4 ${dark ? 'text-white/70 hover:text-white' : 'text-text-secondary hover:text-ink'}`}
>
{secondaryCtaLabel}
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</a>
)}
</motion.div>
</div>
</div>
</div>
</section>
);
}
// ===== Hero 成果带:数据指标下沉为独立带(声明先行,Glance 层滚动触发)=====
// 设计意图:hero 一屏只负责「一句话声明 + 一个行动」,硬指标移出后由滚动触发,
// 既是 Accenture「single stream 逐步展开」的落地,也避免一屏信息过载。
function HeroStatsBand({ stats }: { stats?: Record<string, any>[] }) {
const hasValidStats = stats?.length && stats[0]?.value;
const displayStats = hasValidStats ? stats!.slice(0, 3) : [];
if (displayStats.length === 0) return null;
return (
<section
data-testid="hero-stats"
aria-label="关键成果指标"
className="relative border-t border-border-primary bg-bg-primary"
>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 py-12 sm:py-16">
<ScrollReveal>
<div className="flex flex-wrap gap-8 sm:gap-12 md:gap-16">
{displayStats.map((stat, i) => (
<div key={i} className="flex items-baseline gap-2">
<span className={cn(
'text-3xl sm:text-4xl md:text-5xl font-mono font-bold tabular-nums leading-none',
i === 0 ? 'text-brand-ink' : 'text-ink'
)}>
{stat.value}
</span>
<span className="text-sm text-text-secondary whitespace-nowrap">
{stat.label}
</span>
</div>
))}
</div>
</ScrollReveal>
<MetricsBasisNote basis={weakestBasis(displayStats.map((s) => s.basis))} className="mt-6" />
</div>
</section>
);
}
// ===== 信任/成果区:可验证信任信号 =====
function TrustSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
const eyebrow = pageCopy?.trustEyebrow || '值得信赖';
const titleLines = (pageCopy?.trustTitle || '以可衡量的成果\n赢得长期信任').split('\n');
const signals = (pageCopy?.trustSignals?.length ? pageCopy.trustSignals : TRUST_SIGNALS) as typeof TRUST_SIGNALS;
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary" data-testid="trust-section">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-16 sm:mb-20">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{eyebrow}
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={titleLines} />
</h2>
</ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
{signals.map((item, i) => (
<StaggerReveal key={i} staggerDelay={0.06}>
<div className="bain-card p-6 sm:p-7 h-full flex flex-col">
<div className="flex items-baseline gap-2 mb-3">
<span className="text-xl sm:text-2xl font-black text-ink leading-none">
{item.value}
</span>
<span className="text-sm font-semibold text-text-secondary">{item.label}</span>
</div>
<p className="text-sm text-text-secondary leading-relaxed mb-4 flex-1">{item.desc}</p>
<div className="text-xs text-text-muted border-t border-border-primary pt-3">
数据来源:{canonicalTrustSource(item.source)}
</div>
</div>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
// ===== 章节式叙事区:问题 → 方法 → 结果(Storytelling-Driven)=====
const NARRATIVE_ACTS = [
{
number: '01',
title: '诊断现状',
desc: '从业务目标出发,梳理流程断点、数据孤岛与瓶颈环节,形成可量化的诊断结论。',
href: '/services/consulting',
cta: '了解诊断方法',
},
{
number: '02',
title: '设计路径',
desc: '基于诊断结论设计系统架构与实施路线图,明确每一阶段的交付物与衡量指标。',
href: '/services/solutions',
cta: '查看方案设计',
},
{
number: '03',
title: '交付成果',
desc: '以自研产品组合与专业实施团队完成交付,用真实运营数据验证业务改善,持续迭代。',
href: '/products',
cta: '浏览产品矩阵',
},
];
function NarrativeSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
const narrativeTitle = (pageCopy?.narrativeTitle || '从问题到结果,\n一条可量化的路径').split('\n');
const NARRATIVE_ACTS_CMS = (pageCopy?.narrativeActs?.length ? pageCopy.narrativeActs : NARRATIVE_ACTS) as Array<{
number: string;
title: string;
desc: string;
href: string;
cta: string;
}>;
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-primary" data-testid="narrative-section">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-16 sm:mb-20">
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={narrativeTitle} />
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
{NARRATIVE_ACTS_CMS.map((act) => (
<StaggerReveal key={act.number} staggerDelay={0.05}>
<div className="group relative h-full bg-bg-primary p-8 sm:p-10 md:p-12 transition-colors duration-normal hover:bg-bg-secondary">
<div className="flex items-start justify-between mb-8">
<span
aria-hidden="true"
className="text-4xl sm:text-5xl font-black text-text-hint tabular-nums leading-none"
>
{act.number}
</span>
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-4 group-hover:text-brand-ink transition-colors duration-normal">
{act.title}
</h3>
<p className="text-text-secondary leading-relaxed mb-8 text-sm">
{act.desc}
</p>
<div className="pt-6 border-t border-border-primary">
<a
href={act.href}
className="group/link inline-flex items-center min-h-6 gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors"
>
<span>{act.cta}</span>
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover/link:translate-x-1" />
</a>
</div>
</div>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
// ===== 行业洞察区(对标 Accenture Insights:类型标签 + 内容卡片,思想领导力支柱)=====
function InsightsSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
const eyebrow = (pageCopy?.insightsEyebrow as string) || 'Insights';
const titleLines = ((pageCopy?.insightsTitle as string) || '我们的思考,\n以实践为锚').split('\n');
const desc = (pageCopy?.insightsDescription as string) || '方法论、观点与共创进展——可验证的思考,不空谈趋势。';
const items = (pageCopy?.insightsItems?.length ? pageCopy.insightsItems : FALLBACK_INSIGHTS) as Array<{
tag: string;
title: string;
desc: string;
date?: string;
href: string;
}>;
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-primary" data-testid="insights-section">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 mb-16 sm:mb-20">
<ScrollReveal className="md:max-w-3xl">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{eyebrow}
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={titleLines} />
</h2>
<p className="mt-6 text-base sm:text-lg text-text-secondary leading-relaxed max-w-2xl">
{desc}
</p>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<StaticLink
href="/news"
className="group inline-flex items-center min-h-6 gap-3 text-ink font-semibold text-sm"
>
查看全部洞察
<ArrowRight className="w-4 h-5 transition-transform duration-normal group-hover:translate-x-2 text-text-secondary group-hover:text-brand-ink" />
</StaticLink>
</ScrollReveal>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-px bg-border-primary border border-border-primary">
{items.slice(0, 4).map((insight, i) => (
<StaggerReveal key={i} staggerDelay={0.05}>
<a
href={insight.href}
className="group flex flex-col h-full bg-bg-primary p-8 sm:p-10 md:p-12 transition-colors duration-normal hover:bg-bg-secondary"
>
<div className="flex items-center justify-between mb-8">
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{insight.tag}
</span>
{insight.date && (
<span className="text-xs text-text-muted tabular-nums">{insight.date}</span>
)}
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-4 leading-[1.25] group-hover:text-brand-ink transition-colors duration-normal">
{insight.title}
</h3>
<p className="text-text-secondary leading-relaxed mb-8 text-sm flex-1">
{insight.desc}
</p>
<div className="pt-6 border-t border-border-primary">
<span className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors">
了解详情
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</span>
</div>
</a>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
// ===== 服务区:等宽三列卡片 + 兜底数据 =====
function ServicesSection({ services, pageCopy }: { services?: Record<string, any>[]; pageCopy?: HomePageCopy | null }) {
const hasValidData = services?.length && (services![0] as any)?.subtitle;
const SERVICE_ITEMS = hasValidData
? services!.map((s: Record<string, any>) => ({
...s,
icon: SERVICE_ICON_MAP[s.icon as string] || <Server className="w-7 h-7" />,
})) as Record<string, any>[]
: FALLBACK_SERVICES;
const servicesTitle = (pageCopy?.servicesTitle || '从战略到落地,\n可量化的一站式服务').split('\n');
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="max-w-3xl mb-16 sm:mb-20">
<ScrollReveal>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={servicesTitle} />
</h2>
</ScrollReveal>
</div>
<div className="grid gap-6 sm:grid-cols-2">
{SERVICE_ITEMS.map((service, i) => (
<StaggerReveal key={i} staggerDelay={0.05}>
<a href={service.href} className="group bain-card block p-8 sm:p-10 md:p-12 h-full flex flex-col">
<div className="flex flex-col flex-1">
<div className="flex items-start justify-between mb-10">
<div className="w-12 h-12 flex items-center justify-center bg-bg-secondary text-text-secondary" aria-hidden="true">
{service.icon}
</div>
</div>
<div className="mb-6">
<span className="block text-sm text-text-muted mb-3 font-medium">{service.subtitle}</span>
<h3 className="text-xl lg:text-2xl font-bold text-ink">{service.title}</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-8 text-sm">{service.desc}</p>
<div className="flex flex-wrap gap-2 mb-8">
{(Array.isArray(service.highlights) ? service.highlights : []).map((h: string, j: number) => (
<span key={j} className="px-3 py-1.5 text-xs text-text-muted border border-border-primary font-medium">
{h}
</span>
))}
</div>
{(service as any).metrics && (
<div className="mb-8 pb-8 border-b border-border-primary">
<div className="flex gap-6 mb-2">
{(service as any).metrics.map((m: any, j: number) => (
<div key={j}>
<div className="text-2xl sm:text-3xl font-black text-ink leading-none mb-1">{m.value}</div>
<div className="text-xs text-text-muted font-medium">{m.label}</div>
</div>
))}
</div>
<MetricsBasisNote basis={weakestBasis(((service as any).metrics as any[]).map((m) => m.basis))} />
</div>
)}
<div className="mt-auto pt-2">
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors">
<span>了解详情</span>
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
// ===== 创始人观点区(对标 Accenture CEO Quote:大引言 + 署名,人格化背书)=====
function FounderQuoteSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
const eyebrow = (pageCopy?.founderQuoteEyebrow as string) || '创始团队观点';
const quoteText = (pageCopy?.founderQuoteText as string) || FALLBACK_FOUNDER_QUOTE.text;
const quoteName = (pageCopy?.founderQuoteName as string) || FALLBACK_FOUNDER_QUOTE.name;
const quoteRole = (pageCopy?.founderQuoteRole as string) || FALLBACK_FOUNDER_QUOTE.role;
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-brand-section" data-testid="founder-quote-section">
{/* 背景纹理:极淡白色点阵(Bain 式深红区块 + 克制质感) */}
<div className="absolute inset-0 pointer-events-none opacity-[0.05]"
style={{
backgroundImage: `radial-gradient(circle at 75% 25%, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: '48px 48px',
}}
/>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-4xl mx-auto text-center">
<div className="bg-bg-primary/95 rounded-2xl p-8 sm:p-10 md:p-12 max-w-3xl mx-auto shadow-sm border border-brand/10">
<div className="flex items-center justify-center gap-3 mb-8">
<div className="w-10 h-px bg-brand/40" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{eyebrow}
</span>
<div className="w-10 h-px bg-brand/40" />
</div>
<Quote className="w-10 h-10 mx-auto mb-8 text-brand-ink/40" aria-hidden="true" />
<blockquote className="text-base sm:text-xl md:text-[28px] font-medium text-ink leading-[1.7] mb-10 border-l-0 pl-0 m-0 max-w-[640px] mx-auto whitespace-pre-line">
{quoteText}
</blockquote>
{(quoteName || quoteRole) && (
<div className="flex flex-col items-center gap-1">
{quoteName && (
<div className="text-base font-semibold text-ink">{quoteName}</div>
)}
{quoteRole && (
<div className="text-sm text-text-secondary">{quoteRole}</div>
)}
</div>
)}
</div>
</ScrollReveal>
</div>
</section>
);
}
// ===== 共创流程(可验证方法论,不虚构客户)=====
const CO_CREATION_STEPS = [
{ step: '01', title: '业务诊断', desc: '深入理解业务场景,识别高价值切入点' },
{ step: '02', title: '方案共创', desc: '与核心团队共同设计解决方案与实施路径' },
{ step: '03', title: '验证发布', desc: '以真实运营数据验证成效,经授权后公开案例' },
];
// ===== 案例区:Client Spotlight(对标 Accenture:深色大卡 spotlight + 其余小卡)=====
function CasesSection({ cases, pageCopy }: { cases?: Record<string, any>[]; pageCopy?: HomePageCopy | null }) {
const hasValidData = cases?.length && cases[0]?.metrics;
const CASE_ITEMS = hasValidData ? cases! : [];
const casesEyebrow = pageCopy?.casesEyebrow || '共创进行时';
const casesTitleLines = (pageCopy?.casesTitle || '每一个项目,\n都以业务改善为衡量标准').split('\n');
const casesDesc = pageCopy?.casesDescription || '我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。';
const earlyLabel = pageCopy?.earlyAccessLabel || EARLY_ACCESS.label;
const earlyDesc = pageCopy?.earlyAccessDesc || EARLY_ACCESS.desc;
if (CASE_ITEMS.length === 0) {
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<ScrollReveal>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{casesEyebrow}
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05] mb-8">
<StyledTitle lines={casesTitleLines} />
</h2>
<p className="text-text-secondary leading-relaxed text-lg">
{casesDesc}
</p>
</ScrollReveal>
<ScrollReveal delay={0.15}>
<div className="relative overflow-hidden rounded-[2rem] bg-dark-bg p-10 sm:p-12 h-full flex flex-col shadow-card">
{/* 背景纹理:极淡白色点阵(对标埃森哲深色 spotlight 画布) */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.05]"
style={{
backgroundImage: `radial-gradient(circle at 75% 25%, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: '48px 48px',
}}
/>
<div className="relative z-10 flex flex-col h-full">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-light">
{casesEyebrow}
</span>
</div>
<Quote className="w-8 h-8 mb-6 text-white/30" aria-hidden="true" />
<div className="text-4xl sm:text-5xl font-black text-white tabular-nums leading-none mb-4">
{earlyLabel}
</div>
<p className="text-white/70 leading-relaxed mb-10">{earlyDesc}</p>
<ol className="space-y-5 mb-10">
{CO_CREATION_STEPS.map((s) => (
<li key={s.step} className="flex items-start gap-4">
<span className="text-sm font-black text-brand-light tabular-nums leading-6 shrink-0">
{s.step}
</span>
<div>
<div className="text-sm font-semibold text-white mb-0.5">{s.title}</div>
<div className="text-sm text-white/60 leading-relaxed">{s.desc}</div>
</div>
</li>
))}
</ol>
<div className="mt-auto">
<CTAButton href="/contact" dataTestId="cases-co-creation-cta">
{CTA_LABELS.consult}
</CTAButton>
</div>
</div>
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}
const [spotlightCase, ...restCases] = CASE_ITEMS;
const spotlightTestimonial = spotlightCase?.testimonial as Record<string, any> | undefined;
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 mb-16 sm:mb-20">
<ScrollReveal className="md:max-w-3xl">
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={casesTitleLines} />
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<StaticLink
href="/cases"
className="group inline-flex items-center min-h-6 gap-3 text-ink font-semibold text-sm"
>
查看全部案例
<ArrowRight className="w-4 h-5 transition-transform duration-normal group-hover:translate-x-2 text-text-secondary group-hover:text-brand-ink" />
</StaticLink>
</ScrollReveal>
</div>
{/* Client Spotlight 大卡:深色画布 + 客户引言 + 量化成果(对标 Accenture) */}
{spotlightCase && (
<ScrollReveal className="mb-6">
<a
href={spotlightCase.href || `/cases/${spotlightCase.slug || ''}`}
className="group relative block overflow-hidden rounded-[2rem] bg-dark-bg p-10 sm:p-14 h-full flex flex-col shadow-card"
>
{/* 背景纹理:极淡白色点阵 */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.05]"
style={{
backgroundImage: `radial-gradient(circle at 75% 25%, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: '48px 48px',
}}
/>
<div className="relative z-10 flex flex-col lg:flex-row lg:items-stretch gap-10 lg:gap-16">
<div className="flex flex-col flex-1">
<div className="flex items-center gap-3 mb-8">
<Badge className="bg-white/10 text-white border-transparent">
{spotlightCase.industry}
</Badge>
{spotlightCase.verified && (
<Badge className="bg-brand/15 text-brand-light border-brand/30">
已获客户授权
</Badge>
)}
</div>
<h3 className="text-2xl sm:text-3xl lg:text-4xl font-black text-white leading-[1.15] mb-8">
{spotlightCase.title}
</h3>
{spotlightTestimonial?.quote && (
<blockquote className="max-w-2xl">
<Quote className="w-6 h-6 text-brand-light mb-4" aria-hidden="true" />
<p className="text-white/80 text-lg leading-relaxed">
{spotlightTestimonial.quote}
</p>
{(spotlightTestimonial.author || spotlightTestimonial.name) && (
<footer className="mt-4 text-sm text-white/60">
{spotlightTestimonial.author || spotlightTestimonial.name}
{spotlightTestimonial.role && ` · ${spotlightTestimonial.role}`}
</footer>
)}
</blockquote>
)}
<div className="mt-auto pt-6 border-t border-white/10 flex items-center justify-between gap-6 flex-wrap">
<span className="inline-flex items-center gap-2 text-sm font-semibold text-white group-hover:text-brand-light transition-colors">
查看完整案例
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</span>
</div>
</div>
<div className="lg:w-[38%] shrink-0">
<div className="grid grid-cols-3 lg:grid-cols-1 gap-6 lg:gap-8 h-full">
{spotlightCase.metrics?.slice(0, 3).map((metric: Record<string, any>, j: number) => (
<div key={j} className={cn('lg:py-4', j < 2 && 'lg:border-b lg:border-white/10')}>
<div className={cn(
'text-3xl sm:text-4xl font-black leading-none mb-2 tabular-nums',
j === 0 ? 'text-brand-light' : 'text-white'
)}>
{metric.value}
</div>
<div className="text-sm text-white/60 font-medium leading-tight">
{metric.label}
</div>
</div>
))}
</div>
</div>
</div>
</a>
</ScrollReveal>
)}
{restCases.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{restCases.map((caseItem, i) => (
<ScrollReveal key={i} delay={i * 0.1}>
<a
href={caseItem.href || `/cases/${caseItem.slug || ''}`}
className="group bain-card block p-8 sm:p-10 h-full flex flex-col"
>
<div className="flex items-center gap-3 mb-8">
<Badge className="bg-bg-secondary text-ink border-transparent">
{caseItem.industry}
</Badge>
</div>
<h3 className="text-xl lg:text-2xl font-bold text-ink mb-8 leading-[1.2]">
{caseItem.title}
</h3>
<div className="grid grid-cols-3 gap-4 mb-8">
{(Array.isArray(caseItem.metrics) ? caseItem.metrics : []).slice(0, 3).map((metric: Record<string, any>, j: number) => (
<div key={j}>
<div className={cn(
'text-xl sm:text-2xl font-black leading-none mb-2',
j === 0 ? 'text-brand-ink' : 'text-ink'
)}>
{metric.value}
</div>
<div className="text-xs text-text-secondary font-medium leading-tight">
{metric.label}
</div>
</div>
))}
</div>
<div className="mt-auto pt-4 border-t border-border-primary">
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors">
<span>了解详情</span>
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</div>
</div>
</a>
</ScrollReveal>
))}
</div>
)}
</div>
</section>
);
}
// ===== 新闻动态区(对标 Accenture News:最近 3 条 + 查看全部入口)=====
function NewsSection({ news, pageCopy }: { news?: NewsItem[]; pageCopy?: HomePageCopy | null }) {
const eyebrow = (pageCopy?.newsEyebrow as string) || '新闻动态';
const titleLines = ((pageCopy?.newsTitle as string) || '公司要闻\n与研发动态').split('\n');
const ctaLabel = (pageCopy?.newsCtaLabel as string) || '查看全部新闻';
const NEWS_ITEMS = (news?.length ? news.slice(0, 3) : NEWS.slice(0, 3)) as NewsItem[];
return (
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary" data-testid="news-section">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 mb-16 sm:mb-20">
<ScrollReveal className="md:max-w-3xl">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
{eyebrow}
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
<StyledTitle lines={titleLines} />
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<StaticLink
href="/news"
className="group inline-flex items-center min-h-6 gap-3 text-ink font-semibold text-sm"
>
{ctaLabel}
<ArrowRight className="w-4 h-5 transition-transform duration-normal group-hover:translate-x-2 text-text-secondary group-hover:text-brand-ink" />
</StaticLink>
</ScrollReveal>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{NEWS_ITEMS.map((item, i) => (
<ScrollReveal key={item.id} delay={i * 0.1}>
<StaticLink
href={`/news/${item.id}`}
className="group bain-card block p-8 sm:p-9 h-full flex flex-col"
>
<div className="flex items-center gap-3 mb-6">
<Badge variant="outline" className="text-xs border-brand/30 text-brand-ink">
{item.category}
</Badge>
<span className="flex items-center gap-1.5 text-xs text-text-muted">
<Calendar className="w-3.5 h-3.5" aria-hidden="true" />
{item.date}
</span>
</div>
<h3 className="text-lg lg:text-xl font-bold text-ink mb-4 leading-[1.3] line-clamp-2 group-hover:text-brand-ink transition-colors duration-normal">
{item.title}
</h3>
<p className="text-sm text-text-secondary leading-relaxed mb-6 line-clamp-2 flex-1">
{item.excerpt}
</p>
<div className="pt-4 border-t border-border-primary">
<span className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors">
阅读全文
<ArrowRight className="w-4 h-4 transition-transform duration-normal group-hover:translate-x-1" />
</span>
</div>
</StaticLink>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
// ===== CTA 区:克制强调,品牌红仅用于按钮 =====
function CTASection({ heroData, pageCopy }: { heroData?: Record<string, any> | null; pageCopy?: HomePageCopy | null }) {
const ctaTitleLine1 = heroData?.ctaTitleLine1 || pageCopy?.ctaTitleLine1 || '准备好开始了吗?';
const ctaTitleLine2 = heroData?.ctaTitleLine2 || pageCopy?.ctaTitleLine2 || '让数据真正转化为业务增长';
const ctaLabel = canonicalCtaLabel(heroData?.ctaLabel || (pageCopy?.ctaLabel as string)) || CTA_LABELS.consult;
const ctaHref = heroData?.ctaHref || '/contact';
return (
<section className="relative py-24 sm:py-32 md:py-40 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl">
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05] mb-10 sm:mb-14">
{ctaTitleLine1}
<br />
{ctaTitleLine2}
</h2>
<CTAButton href={ctaHref}>{ctaLabel}</CTAButton>
</ScrollReveal>
</div>
</section>
);
}
// ===== 主页面组件 =====
export default function HomeContentV15({ services, cases, stats, heroData, news, pageCopy }: {
services?: Record<string, any>[];
cases?: Record<string, any>[];
stats?: Record<string, any>[];
heroData?: Record<string, any> | null;
news?: NewsItem[];
pageCopy?: HomePageCopy | null;
}) {
return (
<div>
{/* 1. Hero:浅色 editorial 画布 + 品牌红单电压(参考 Accenture Radical Relevance) */}
<HeroSection heroData={heroData} />
{/* 1.5 成果带:数据指标下沉,滚动触发(Glance 层,声明先行的承接) */}
<HeroStatsBand stats={stats} />
{/* 2. 信任区:量化数据成果展示 */}
<TrustSection pageCopy={pageCopy} />
{/* 3. 章节式叙事:问题 → 方法 → 结果 */}
<NarrativeSection pageCopy={pageCopy} />
{/* 4. 行业洞察:思想领导力支柱(对标 Accenture Insights) */}
<InsightsSection pageCopy={pageCopy} />
{/* 5. 服务区:等宽卡片 + 兜底数据 */}
<ServicesSection services={services} pageCopy={pageCopy} />
{/* 6. 创始人观点:人格化背书(对标 Accenture CEO Quote) */}
<FounderQuoteSection pageCopy={pageCopy} />
{/* 7. 案例区:轻量快照卡片 */}
<CasesSection cases={cases} pageCopy={pageCopy} />
{/* 8. 新闻动态:持续动态(对标 Accenture News) */}
<NewsSection news={news} pageCopy={pageCopy} />
{/* 9. CTA:克制强调,品牌红仅用于按钮 */}
<CTASection heroData={heroData} pageCopy={pageCopy} />
</div>
);
}