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:
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ScrollReveal } from '@/components/ui/scroll-reveal';
|
||||
import { ArrowRight, Shield, Building2, Users, Code, Target, Layers, BookOpen, Sparkles, type LucideIcon } from 'lucide-react';
|
||||
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
||||
import { EASE_OUT } from '@/components/ui/page-decoration';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
/** Icon name-to-component mapping for CMS string-based icons */
|
||||
@@ -33,12 +32,6 @@ interface CultureData {
|
||||
iconName: string;
|
||||
}
|
||||
|
||||
interface StatData {
|
||||
value: number;
|
||||
suffix: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ParagraphData {
|
||||
text: string;
|
||||
}
|
||||
@@ -53,7 +46,7 @@ interface TeamData {
|
||||
heroSecondaryCtaHref?: string;
|
||||
strengths?: StrengthData[];
|
||||
culture?: CultureData[];
|
||||
stats?: StatData[];
|
||||
stats?: { value: number; suffix: string; label: string }[];
|
||||
aboutTitle?: string;
|
||||
aboutParagraphs?: ParagraphData[];
|
||||
ctaTitle?: string;
|
||||
@@ -64,58 +57,6 @@ interface TeamData {
|
||||
ctaSecondaryHref?: string;
|
||||
}
|
||||
|
||||
function useCountUp(target: number, start: boolean, duration: number = 2000) {
|
||||
const [count, setCount] = useState(0);
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
if (!start || prefersReducedMotion) {
|
||||
setCount(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
let animationId: number;
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
setCount(Math.floor(target * easeOutQuart));
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
} else {
|
||||
setCount(target);
|
||||
}
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(animationId);
|
||||
}, [target, start, duration, prefersReducedMotion]);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function StatItem({ value, suffix, label, start, delay }: { value: number; suffix: string; label: string; start: boolean; delay: number }) {
|
||||
const count = useCountUp(value, start, 2000);
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={start ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ delay, duration: 0.6, ease: EASE_OUT }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="text-4xl sm:text-5xl font-bold text-ink tracking-tight leading-none mb-2">
|
||||
{count}<span className="text-brand">{suffix}</span>
|
||||
</div>
|
||||
<div className="text-sm text-text-muted tracking-wide">{label}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function StrengthCard({ strength, index }: { strength: StrengthData; index: number }) {
|
||||
const Icon = ICON_MAP[strength.iconName] ?? Shield;
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
@@ -183,8 +124,6 @@ function CultureCard({ culture, index }: { culture: CultureData; index: number }
|
||||
|
||||
export default function TeamContentV3({ data }: { data: Record<string, unknown> }) {
|
||||
const teamData = data as TeamData;
|
||||
const statsRef = useRef(null);
|
||||
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
|
||||
|
||||
const strengths = teamData.strengths ?? [];
|
||||
const culture = teamData.culture ?? [];
|
||||
@@ -202,81 +141,120 @@ export default function TeamContentV3({ data }: { data: Record<string, unknown>
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
|
||||
{/* Hero Section */}
|
||||
{/* ============ L1: Hero 团队情感入口 ============ */}
|
||||
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
||||
<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">
|
||||
<ScrollReveal>
|
||||
<SectionLabel>{teamData.heroSubtitle || ''}</SectionLabel>
|
||||
</ScrollReveal>
|
||||
{/* 眉标 Badge */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: EASE_OUT }}
|
||||
className="mb-8 sm:mb-10"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand/[0.08] border border-brand/20 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand" />
|
||||
<span className="text-[13px] font-semibold text-brand tracking-[0.08em] uppercase">
|
||||
{teamData.heroSubtitle || '核心团队'}
|
||||
</span>
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<ScrollReveal delay={0.1}>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
|
||||
{(teamData.heroTitle || '').split('\n').map((line, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && <span className="block mt-4" />}
|
||||
{i === 0 ? line : <span className="text-brand">{line}</span>}
|
||||
</span>
|
||||
{/* 主标题 — Bain 风格:大号加粗 + 品牌红关键词 */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, 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"
|
||||
>
|
||||
{(teamData.heroTitle || '').split('\n').map((line, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && <span className="block mt-4 sm:mt-6" />}
|
||||
{i === 0 ? line : <span className="text-brand">{line}</span>}
|
||||
</span>
|
||||
))}
|
||||
</motion.h1>
|
||||
|
||||
{/* 数据指标条 — Bain 风格:答案优先,数据先行 */}
|
||||
{stats.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.4, ease: EASE_OUT }}
|
||||
className="flex flex-wrap gap-6 sm:gap-10 md:gap-14 mb-10 sm:mb-12"
|
||||
>
|
||||
{stats.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}{stat.suffix}
|
||||
</span>
|
||||
<span className="text-sm text-text-muted whitespace-nowrap">
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</h1>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<ScrollReveal delay={0.2}>
|
||||
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12">
|
||||
{teamData.heroDescription || ''}
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
{/* 描述 */}
|
||||
{teamData.heroDescription && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.5, ease: EASE_OUT }}
|
||||
className="text-lg sm:text-xl text-text-secondary/70 max-w-2xl leading-relaxed mb-12 sm:mb-14"
|
||||
>
|
||||
{teamData.heroDescription}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
<ScrollReveal delay={0.3}>
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 mb-16">
|
||||
<a
|
||||
href={teamData.heroPrimaryCtaHref || '#'}
|
||||
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
|
||||
>
|
||||
{teamData.heroPrimaryCtaLabel || ''}
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
{/* CTA 按钮组 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.6, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6"
|
||||
>
|
||||
<a
|
||||
href={teamData.heroPrimaryCtaHref || '/contact'}
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px]"
|
||||
>
|
||||
{teamData.heroPrimaryCtaLabel || '加入我们'}
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
{teamData.heroSecondaryCtaLabel && (
|
||||
<a
|
||||
href={teamData.heroSecondaryCtaHref || '#'}
|
||||
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
|
||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300 min-h-[52px]"
|
||||
>
|
||||
{teamData.heroSecondaryCtaLabel || ''}
|
||||
{teamData.heroSecondaryCtaLabel}
|
||||
</a>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{stats.length > 0 && (
|
||||
<div ref={statsRef} className="grid grid-cols-3 gap-6 sm:gap-8 md:gap-12 max-w-2xl">
|
||||
{stats.map((stat, idx) => (
|
||||
<StatItem
|
||||
key={stat.label}
|
||||
value={stat.value}
|
||||
suffix={stat.suffix}
|
||||
label={stat.label}
|
||||
start={isInView}
|
||||
delay={idx * 0.15}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Team Strengths Section */}
|
||||
{/* ============ L2: 团队实力理性支撑 ============ */}
|
||||
{strengths.length > 0 && (
|
||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
||||
<section className="relative py-24 sm:py-32 md: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 relative z-10">
|
||||
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
|
||||
<SectionLabel className="justify-center">Team Strengths</SectionLabel>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
|
||||
我们的底气,从何而来
|
||||
<ScrollReveal className="max-w-3xl 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.15em] font-medium text-brand">
|
||||
团队实力
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||
12 年深耕,
|
||||
<span className="text-brand">500+ 项目验证</span>
|
||||
的交付能力
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
||||
不是自吹自擂,而是多年实战积累的真实能力
|
||||
<p className="text-text-secondary leading-relaxed mt-4 text-base sm:text-lg">
|
||||
复合型专家团队,从战略咨询到技术落地,全链路覆盖
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -291,11 +269,20 @@ export default function TeamContentV3({ data }: { data: Record<string, unknown>
|
||||
|
||||
{/* Team About Section */}
|
||||
{(teamData.aboutTitle || aboutParagraphs.length > 0) && (
|
||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
||||
<section className="relative py-24 sm:py-32 md:py-40 overflow-hidden bg-bg-secondary">
|
||||
<div className="absolute top-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 relative z-10">
|
||||
<ScrollReveal>
|
||||
<div className="max-w-4xl mx-auto p-8 sm:p-10 lg:p-16 border border-border-primary bg-bg-secondary/50">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-8 text-center text-ink">
|
||||
<div className="max-w-4xl mx-auto p-8 sm:p-10 lg:p-16 border border-border-primary bg-white">
|
||||
<div className="flex items-center gap-3 mb-8 justify-center">
|
||||
<div className="w-10 h-px bg-brand" />
|
||||
<span className="text-[13px] tracking-[0.15em] font-medium text-brand">
|
||||
关于我们
|
||||
</span>
|
||||
<div className="w-10 h-px bg-brand" />
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-black text-ink tracking-tight mb-8 text-center">
|
||||
{teamData.aboutTitle || ''}
|
||||
</h2>
|
||||
<div className="space-y-6 max-w-3xl mx-auto text-center">
|
||||
@@ -311,19 +298,24 @@ export default function TeamContentV3({ data }: { data: Record<string, unknown>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Team Culture Section */}
|
||||
{/* ============ L3: 团队文化信任证明 ============ */}
|
||||
{culture.length > 0 && (
|
||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
||||
<section className="relative py-24 sm:py-32 md:py-40 overflow-hidden bg-white">
|
||||
<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 relative z-10">
|
||||
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20">
|
||||
<SectionLabel className="justify-center">Team Culture</SectionLabel>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
|
||||
我们怎样工作
|
||||
<ScrollReveal className="max-w-3xl 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.15em] font-medium text-brand">
|
||||
团队文化
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||
专业文化,让每个项目都成为
|
||||
<span className="text-brand">行业标杆</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
||||
<p className="text-text-secondary leading-relaxed mt-4 text-base sm:text-lg">
|
||||
好的团队文化,最终体现在为客户创造的价值上
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
@@ -337,32 +329,36 @@ export default function TeamContentV3({ data }: { data: Record<string, unknown>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
||||
<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" />
|
||||
{/* ============ L4: CTA 团队行动号召 ============ */}
|
||||
<section className="relative py-24 sm:py-32 md:py-44 overflow-hidden bg-dark-bg">
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/3 right-1/4 w-[500px] h-[500px] bg-brand/[0.04] rounded-full blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||
<ScrollReveal className="text-center max-w-3xl mx-auto">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight text-ink">
|
||||
{teamData.ctaTitle || ''}
|
||||
<ScrollReveal className="max-w-3xl">
|
||||
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl xl:text-7xl font-black text-dark-text-primary tracking-tightest leading-[0.9] mb-8 sm:mb-10">
|
||||
{teamData.ctaTitle || '成为下一个'}
|
||||
<br />
|
||||
<span className="text-brand">数据驱动型企业</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
||||
{teamData.ctaDescription || ''}
|
||||
<p className="text-dark-text-secondary text-lg leading-relaxed mb-12 sm:mb-16 max-w-xl">
|
||||
{teamData.ctaDescription || '无论您处于数字化转型的哪个阶段,我们都愿意成为您的长期伙伴。'}
|
||||
</p>
|
||||
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
href={teamData.ctaPrimaryHref || '#'}
|
||||
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90"
|
||||
href={teamData.ctaPrimaryHref || '/contact'}
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px]"
|
||||
>
|
||||
{teamData.ctaPrimaryLabel || ''}
|
||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
{teamData.ctaPrimaryLabel || '预约咨询'}
|
||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
<a
|
||||
href={teamData.ctaSecondaryHref || '#'}
|
||||
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary transition-all duration-300"
|
||||
href={teamData.ctaSecondaryHref || '/cases'}
|
||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-dark-text-primary border border-dark-border hover:border-dark-text-muted transition-all duration-300 min-h-[52px]"
|
||||
>
|
||||
{teamData.ctaSecondaryLabel || ''}
|
||||
{teamData.ctaSecondaryLabel || '查看客户案例'}
|
||||
</a>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
Reference in New Issue
Block a user