Dogfood 审计(31 路由 × 多视口,Playwright 9 维探针)发现的阻断级问题全部闭环: P1 29 → 0,对比度违规页 29 → 0。 对比度 - footer: text-gray-500 硬编码 → text-text-hint 令牌(#6B7280 在 #0A0E14 上仅 4:1,需 4.5:1),1 行改动全站 29 页生效 - brand-content / team-content 副文案 text-text-secondary/70 → /80 (4.35:1 → 5.74:1,视觉几乎无变化) - brand-content 里程碑标签在 bg-tertiary 上 text-text-muted → text-text-secondary (4.34:1 → 9.45:1) 装饰元素豁免 - 大号服务编号(about 01-03、team 01-05 等)加 aria-hidden="true",WCAG 1.4.3 对装饰内容不适用,无需强行提亮 - 同时补修 product-detail / solution-detail 的客户名首字占位(同类 text-text-muted/10,因容器为渐变背景属探针盲区、从未报出) - 副文案与里程碑标签属真实语义内容,走提对比度而非 aria-hidden —— 隐藏它们 等于把公司简介与成立年份从屏幕阅读器中删除 移动端布局 - MobileTabBar 为 fixed bottom-0 高 64px,给 main / [role="main"] / footer 补 calc(64px + env(safe-area-inset-bottom)) 安全区,解决正文与页脚被遮挡 - 注意 marketing layout 使用 div[role="main"] 而非真 <main> 元素,选择器须 同时匹配两者 触控与令牌 - ERP 专题链接加 py-0.5(23px → 27px,达 WCAG 2.2 AA 2.5.8 的 24px) - solution-service-card: shadow-blue-500/20 → shadow-brand(纳入 --shadow-* 令牌) Refs: UI_UX_UE_AUDIT_2026-09-01.md
359 lines
15 KiB
TypeScript
359 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ScrollReveal } from '@/components/ui/scroll-reveal';
|
|
import { CTAButton } from '@/components/ui/cta-button';
|
|
import { Shield, Building2, Users, Code, Target, Layers, BookOpen, Sparkles, type LucideIcon } from 'lucide-react';
|
|
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 */
|
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
Shield,
|
|
Building2,
|
|
Users,
|
|
Code,
|
|
Target,
|
|
Layers,
|
|
BookOpen,
|
|
Sparkles,
|
|
};
|
|
|
|
interface StrengthData {
|
|
number: string;
|
|
title: string;
|
|
description: string;
|
|
iconName: string;
|
|
}
|
|
|
|
interface CultureData {
|
|
number: string;
|
|
title: string;
|
|
description: string;
|
|
iconName: string;
|
|
}
|
|
|
|
interface ParagraphData {
|
|
text: string;
|
|
}
|
|
|
|
interface TeamData {
|
|
heroSubtitle?: string;
|
|
heroTitle?: string;
|
|
heroDescription?: string;
|
|
heroPrimaryCtaLabel?: string;
|
|
heroPrimaryCtaHref?: string;
|
|
heroSecondaryCtaLabel?: string;
|
|
heroSecondaryCtaHref?: string;
|
|
strengths?: StrengthData[];
|
|
culture?: CultureData[];
|
|
stats?: { value: number; suffix: string; label: string }[];
|
|
aboutTitle?: string;
|
|
aboutParagraphs?: ParagraphData[];
|
|
ctaTitle?: string;
|
|
ctaDescription?: string;
|
|
ctaPrimaryLabel?: string;
|
|
ctaPrimaryHref?: string;
|
|
ctaSecondaryLabel?: string;
|
|
ctaSecondaryHref?: string;
|
|
}
|
|
|
|
function StrengthCard({ strength, index }: { strength: StrengthData; index: number }) {
|
|
const Icon = ICON_MAP[strength.iconName] ?? Shield;
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.div
|
|
key={strength.title}
|
|
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 overflow-hidden"
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-80px' }}
|
|
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
|
|
whileHover={{ y: -6 }}
|
|
>
|
|
<div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
|
|
|
|
<div className="p-8 sm:p-10">
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="w-12 h-12 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
{/* 纯装饰序号:与右侧 h3 标题并列,无独立语义。
|
|
aria-hidden 让 WCAG 1.4.3 对比度要求不适用于装饰内容(1.13:1 无需强行提亮) */}
|
|
<span aria-hidden="true" className="text-4xl font-bold text-text-muted/10 font-mono">{strength.number}</span>
|
|
</div>
|
|
|
|
<h3 className="text-xl font-bold text-ink mb-3">{strength.title}</h3>
|
|
<p className="text-text-secondary text-sm leading-relaxed">{strength.description}</p>
|
|
</div>
|
|
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-brand/40 via-brand/10 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function CultureCard({ culture, index }: { culture: CultureData; index: number }) {
|
|
const Icon = ICON_MAP[culture.iconName] ?? Layers;
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.div
|
|
key={culture.title}
|
|
className="group relative border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 text-center overflow-hidden"
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-80px' }}
|
|
transition={{ duration: 0.6, delay: index * 0.1, ease: EASE_OUT }}
|
|
whileHover={{ y: -8 }}
|
|
>
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
|
|
|
|
<div className="p-8 sm:p-10 lg:p-12">
|
|
<div className="w-16 h-16 mx-auto mb-6 flex items-center justify-center border border-brand/30 bg-brand/[0.08] text-brand">
|
|
<Icon className="w-7 h-7" />
|
|
</div>
|
|
|
|
<span aria-hidden="true" className="block text-6xl font-bold text-text-muted/10 font-mono mb-4">{culture.number}</span>
|
|
|
|
<h3 className="text-2xl font-bold text-ink mb-4">{culture.title}</h3>
|
|
<p className="text-text-secondary leading-relaxed">{culture.description}</p>
|
|
</div>
|
|
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand/20 to-transparent scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export default function TeamContentV3({ data }: { data: Record<string, unknown> }) {
|
|
const teamData = data as TeamData;
|
|
|
|
const strengths = teamData.strengths ?? [];
|
|
const culture = teamData.culture ?? [];
|
|
const stats = teamData.stats ?? [];
|
|
const aboutParagraphs = teamData.aboutParagraphs ?? [];
|
|
|
|
// Empty state when no CMS data is available
|
|
if (!data || Object.keys(data).length === 0) {
|
|
return (
|
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
|
<div className="text-text-muted text-lg">内容暂未发布</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
|
|
{/* ============ 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">
|
|
{/* 眉标 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>
|
|
|
|
{/* 主标题 — 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>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* 描述 — alpha 0.7 仅 4.35:1 未达 WCAG AA 4.5:1,提到 0.8 → 5.74:1,视觉几乎无变化 */}
|
|
{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/80 max-w-2xl leading-relaxed mb-12 sm:mb-14"
|
|
>
|
|
{teamData.heroDescription}
|
|
</motion.p>
|
|
)}
|
|
|
|
{/* 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"
|
|
>
|
|
<CTAButton href={teamData.heroPrimaryCtaHref || '/contact'}>
|
|
{teamData.heroPrimaryCtaLabel || '加入我们'}
|
|
</CTAButton>
|
|
{teamData.heroSecondaryCtaLabel && (
|
|
<CTAButton href={teamData.heroSecondaryCtaHref || '#'} variant="secondary">
|
|
{teamData.heroSecondaryCtaLabel}
|
|
</CTAButton>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ============ L2: 团队实力理性支撑 ============ */}
|
|
{strengths.length > 0 && (
|
|
<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 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="text-text-secondary leading-relaxed mt-4 text-base sm:text-lg">
|
|
复合型专家团队,从战略咨询到技术落地,全链路覆盖
|
|
</p>
|
|
</ScrollReveal>
|
|
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
|
{strengths.map((strength, idx) => (
|
|
<StrengthCard key={strength.title} strength={strength} index={idx} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Team About Section */}
|
|
{(teamData.aboutTitle || aboutParagraphs.length > 0) && (
|
|
<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-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">
|
|
{aboutParagraphs.map((p, i) => (
|
|
<p key={i} className="text-text-secondary leading-relaxed text-base sm:text-lg">
|
|
{p.text}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</ScrollReveal>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* ============ L3: 团队文化信任证明 ============ */}
|
|
{culture.length > 0 && (
|
|
<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="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
|
<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="text-text-secondary leading-relaxed mt-4 text-base sm:text-lg">
|
|
好的团队文化,最终体现在为客户创造的价值上
|
|
</p>
|
|
</ScrollReveal>
|
|
|
|
<div className="grid md:grid-cols-3 gap-8">
|
|
{culture.map((item, idx) => (
|
|
<CultureCard key={item.title} culture={item} index={idx} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* ============ 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="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="text-dark-text-secondary text-lg leading-relaxed mb-12 sm:mb-16 max-w-xl">
|
|
{teamData.ctaDescription || '无论您处于数字化转型的哪个阶段,我们都愿意成为您的长期伙伴。'}
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
|
<CTAButton href={teamData.ctaPrimaryHref || '/contact'}>
|
|
{teamData.ctaPrimaryLabel || '预约咨询'}
|
|
</CTAButton>
|
|
<CTAButton href={teamData.ctaSecondaryHref || '/cases'} variant="dark">
|
|
{teamData.ctaSecondaryLabel || '查看客户案例'}
|
|
</CTAButton>
|
|
</div>
|
|
</ScrollReveal>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|