chore: sync marketing pages, CMS extensions, tests and project docs
同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
'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 { ArrowRight, Lightbulb, Database, Layers } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EASE_OUT } from '@/components/ui/page-decoration';
|
||||
|
||||
// ===== 服务数据降级兜底 =====
|
||||
const FALLBACK_SERVICES = [
|
||||
{
|
||||
subtitle: 'Strategy Consulting',
|
||||
title: '数字化转型战略咨询',
|
||||
desc: '从业务诊断到数字化路线图,帮助企业识别高价值场景,规划可落地的转型路径,确保每一分投入都能产生可衡量的业务回报。',
|
||||
highlights: ['业务诊断', '数字化蓝图', '实施路线图', 'ROI 评估'],
|
||||
metrics: [
|
||||
{ value: '40%', label: '平均运营效率提升' },
|
||||
{ value: '6个月', label: '战略到试点周期' },
|
||||
],
|
||||
href: '/services/strategy',
|
||||
icon: <Lightbulb 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: '500+', label: '项目交付经验' },
|
||||
{ value: '99.9%', label: '系统可用性' },
|
||||
],
|
||||
href: '/services/solution',
|
||||
icon: <Layers className="w-7 h-7" />,
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 品牌 CTA 按钮 =====
|
||||
function CTAButton({ children, href, variant = 'primary', className }: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
variant?: 'primary' | 'secondary';
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-3 px-10 py-4 font-semibold text-base transition-all duration-200',
|
||||
variant === 'primary'
|
||||
? 'bg-brand text-white hover:bg-brand-hover'
|
||||
: 'text-ink border border-ink/20 hover:border-ink/40 hover:bg-ink/5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-2" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Hero 区:克制白底 + 品牌红点缀 =====
|
||||
function HeroSection({ heroData, stats }: {
|
||||
heroData?: Record<string, any> | null;
|
||||
stats?: Record<string, any>[];
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const heading = heroData?.heading || heroData?.headingBottom || '从数据到决策,让转型可量化';
|
||||
const subheading = heroData?.subheading || heroData?.description || '';
|
||||
const ctaLabel = heroData?.ctaLabel || '预约咨询';
|
||||
const ctaHref = heroData?.ctaHref || '/contact';
|
||||
const secondaryCtaLabel = heroData?.secondaryCtaLabel || '';
|
||||
const secondaryCtaHref = heroData?.secondaryCtaHref || '#';
|
||||
|
||||
const hasValidStats = stats?.length && stats[0]?.value;
|
||||
const displayStats = hasValidStats ? stats!.slice(0, 3) : [];
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
className="relative min-h-[90vh] flex items-center overflow-hidden bg-white"
|
||||
>
|
||||
{/* 背景纹理:极淡的品牌色点阵 */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 25% 25%, rgba(196,30,58,0.4) 1px, transparent 1px)`,
|
||||
backgroundSize: '48px 48px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-32 md:py-40 lg:py-48">
|
||||
<div className="max-w-4xl">
|
||||
{/* 主标题:大胆、直接 */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
|
||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-black text-ink leading-[1.05] tracking-tight mb-10"
|
||||
>
|
||||
{heading}
|
||||
</motion.h1>
|
||||
|
||||
{/* 数据指标条:答案优先,数据先行 */}
|
||||
{displayStats.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.3, ease: EASE_OUT }}
|
||||
className="flex flex-wrap gap-8 sm:gap-12 md:gap-16 mb-10"
|
||||
>
|
||||
{displayStats.map((stat, i) => (
|
||||
<div key={i} className="flex items-baseline gap-2">
|
||||
<span className="text-3xl sm:text-4xl md:text-5xl font-black text-brand tabular-nums leading-none">
|
||||
{stat.value}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 副标题 */}
|
||||
{subheading && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.4, ease: EASE_OUT }}
|
||||
className="text-lg sm:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
|
||||
>
|
||||
{subheading}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* CTA 按钮组 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.5, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6"
|
||||
>
|
||||
<CTAButton href={ctaHref}>{ctaLabel}</CTAButton>
|
||||
{secondaryCtaLabel && (
|
||||
<a
|
||||
href={secondaryCtaHref}
|
||||
className="group inline-flex items-center gap-2 text-sm font-medium text-text-secondary hover:text-ink transition-colors duration-200 py-4"
|
||||
>
|
||||
{secondaryCtaLabel}
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 信任/成果区:Bain 式量化数据展示 =====
|
||||
function TrustSection() {
|
||||
const items = [
|
||||
{ value: '12+', label: '年行业深耕', desc: '自 2014 年起专注企业数字化转型' },
|
||||
{ value: '500+', label: '项目交付', desc: '覆盖金融、制造、政务等 20+ 行业' },
|
||||
{ value: '98%', label: '客户续约率', desc: '长期陪跑,持续创造业务价值' },
|
||||
{ value: '100%', label: '私有化部署', desc: '核心数据不出境,满足合规要求' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-white">
|
||||
<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">
|
||||
值得信赖
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||
以可衡量的成果
|
||||
<br />
|
||||
赢得长期信任
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{items.map((item, i) => (
|
||||
<StaggerReveal key={i} staggerDelay={0.06}>
|
||||
<div className="bain-card p-8 sm:p-10">
|
||||
<div className="text-3xl sm:text-4xl font-black text-brand tabular-nums leading-none mb-2">
|
||||
{item.value}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-ink mb-3">{item.label}</div>
|
||||
<p className="text-sm text-text-secondary leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</StaggerReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 服务区:等宽三列卡片 + 兜底数据 =====
|
||||
function ServicesSection({ services }: { services?: Record<string, any>[] }) {
|
||||
const hasValidData = services?.length && services[0]?.title;
|
||||
const SERVICE_ITEMS = hasValidData ? services! : FALLBACK_SERVICES;
|
||||
|
||||
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]">
|
||||
从战略到落地,
|
||||
<br />
|
||||
可量化的一站式服务
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{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">
|
||||
{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="flex gap-6 mb-8 pb-8 border-b border-border-primary">
|
||||
{(service as any).metrics.map((m: any, j: number) => (
|
||||
<div key={j}>
|
||||
<div className="text-2xl sm:text-3xl font-black text-brand leading-none mb-1">{m.value}</div>
|
||||
<div className="text-[11px] text-text-muted font-medium">{m.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</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 transition-colors">
|
||||
<span>了解详情</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</StaggerReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 案例区:轻量快照卡片 =====
|
||||
function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
||||
const hasValidData = cases?.length && cases[0]?.metrics;
|
||||
const CASE_ITEMS = hasValidData ? cases! : [];
|
||||
|
||||
if (CASE_ITEMS.length === 0) {
|
||||
return (
|
||||
<section className="relative py-28 overflow-hidden bg-white">
|
||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
|
||||
<p className="text-text-muted text-lg">暂无案例数据</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-white">
|
||||
<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]">
|
||||
每一个项目,
|
||||
<br />
|
||||
都以业务改善为衡量标准
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.1}>
|
||||
<a
|
||||
href="/cases"
|
||||
className="group inline-flex items-center gap-3 text-ink font-semibold text-sm"
|
||||
>
|
||||
查看全部案例
|
||||
<ArrowRight className="w-4 h-5 transition-transform duration-300 group-hover:translate-x-2 text-text-secondary group-hover:text-brand" />
|
||||
</a>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{CASE_ITEMS.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">
|
||||
{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' : '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 transition-colors">
|
||||
<span>了解详情</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== CTA 区:克制强调,品牌红仅用于按钮 =====
|
||||
function CTASection({ heroData }: { heroData?: Record<string, any> | null }) {
|
||||
const ctaTitleLine1 = heroData?.ctaTitleLine1 || '准备好开始了吗?';
|
||||
const ctaTitleLine2 = heroData?.ctaTitleLine2 || '让数据真正转化为业务增长';
|
||||
const ctaLabel = heroData?.ctaLabel || '预约咨询';
|
||||
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 HomeContentV14({ services, cases, stats, heroData }: {
|
||||
services?: Record<string, any>[];
|
||||
cases?: Record<string, any>[];
|
||||
stats?: Record<string, any>[];
|
||||
heroData?: Record<string, any> | null;
|
||||
}) {
|
||||
return (
|
||||
<main>
|
||||
{/* 1. Hero:克制白底 + 品牌红点缀 */}
|
||||
<HeroSection heroData={heroData} stats={stats} />
|
||||
|
||||
{/* 2. 信任区:量化数据成果展示 */}
|
||||
<TrustSection />
|
||||
|
||||
{/* 3. 服务区:等宽卡片 + 兜底数据 */}
|
||||
<ServicesSection services={services} />
|
||||
|
||||
{/* 4. 案例区:轻量快照卡片 */}
|
||||
<CasesSection cases={cases} />
|
||||
|
||||
{/* 5. CTA:克制强调,品牌红仅用于按钮 */}
|
||||
<CTASection heroData={heroData} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user