feat(home): hero 信息层重构 — 声明先行,数据指标下沉为独立成果带
参考 Accenture Radical Relevance 设计语言(方向 A,用户已决策): - hero 内数据条移出,主张(副标题)前置,一屏只负责一句话声明 + 一个行动 - 新增 HeroStatsBand 成果带,滚动触发(Glance 层),承接下沉的指标 - H1 字重 900→700,hero 内 KPI 数字同步收敛,对齐「字重 9→3」克制 - 右侧数据看板保持单电压(仅品牌红),hero 内已无辅助色 验证(evidence-first): - type-check 0 errors / lint 0 errors(127 warnings 均为既有、与本改无关) - jest 13 passed(含新断言:hero 内不含 hero-stats、数据下沉成果带仍渲染) - 视觉回归 22 passed(首页浅/深快照已 update;19 个非首页页零 diff,证明改动仅影响首页) 含 docs/design/hero-redesign-proposal.md(设计提案,未改代码) P0 批次;P1(点阵 token 化 + L2 光标聚光)待续
This commit is contained in:
@@ -106,9 +106,13 @@ describe('HomeContentV15 - 浅色 Hero(暗黑模式 token 友好)', () => {
|
||||
expect(screen.getByTestId('hero-secondary-link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the answer-first stats bar visible', () => {
|
||||
it('keeps hero declaration-first: stats sink out of hero into the results band', () => {
|
||||
render(<HomeContentV15 stats={[{ value: '100%', label: '私有化部署' }]} />);
|
||||
|
||||
const hero = screen.getByTestId('hero-section');
|
||||
// 声明先行:hero 内不再内联数据条(一屏只负责一句话声明 + 一个行动)
|
||||
expect(hero.querySelector('[data-testid="hero-stats"]')).toBeNull();
|
||||
// 数据下沉为独立成果带,仍渲染(Glance 层滚动触发)
|
||||
expect(screen.getByTestId('hero-stats')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -135,10 +135,14 @@ function StyledTitle({ lines }: { lines: (string | undefined)[] }) {
|
||||
|
||||
// ===== 品牌 CTA 按钮已抽离为可复用 UI 组件:@/components/ui/cta-button =====
|
||||
|
||||
// ===== Hero 区:全出血深色画布(对标 Accenture 纯黑 Hero)+ 品牌红单电压 =====
|
||||
function HeroSection({ heroData, stats }: {
|
||||
// ===== Hero 区:浅色 editorial 画布 + 品牌红单电压 =====
|
||||
// 设计方向(2026-08-31 重设计,参考 Accenture Radical Relevance):
|
||||
// - 声明先行:H1 → 主张 → CTA,一屏只负责「一句话说清你是谁 + 一个行动」
|
||||
// - 数据指标下沉为独立成果带(HeroStatsBand),由滚动触发(Glance 层),不再挤在 hero 内
|
||||
// - 单一电压:hero 内只有品牌红一个彩色,其余全走中性 token
|
||||
// - 字重收敛:大标题 700(原 900),对齐 Accenture「字重 9→3」的克制
|
||||
function HeroSection({ heroData }: {
|
||||
heroData?: Record<string, any> | null;
|
||||
stats?: Record<string, any>[];
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const siteConfig = useSiteConfig();
|
||||
@@ -151,9 +155,6 @@ function HeroSection({ heroData, stats }: {
|
||||
const secondaryCtaHref = heroData?.secondaryCtaHref || '/products';
|
||||
const slogan = siteConfig.slogan || COMPANY_INFO.slogan;
|
||||
|
||||
const hasValidStats = stats?.length && stats[0]?.value;
|
||||
const displayStats = hasValidStats ? stats!.slice(0, 3) : [];
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
@@ -190,39 +191,17 @@ function HeroSection({ heroData, stats }: {
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.45, delay: 0.06, ease: EASE_OUT }}
|
||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-6xl 2xl:text-7xl font-black text-ink leading-[1.05] tracking-tight mb-8 sm:mb-10"
|
||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-6xl 2xl:text-7xl font-bold text-ink leading-[1.05] tracking-tight mb-7 sm:mb-8"
|
||||
>
|
||||
{heading}
|
||||
</motion.h1>
|
||||
|
||||
{/* 数据指标条:答案优先,数据先行(品牌红仅用于第一个数字) */}
|
||||
{displayStats.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, delay: 0.16, ease: EASE_OUT }}
|
||||
className="flex flex-wrap gap-8 sm:gap-12 md:gap-16 mb-8"
|
||||
data-testid="hero-stats"
|
||||
>
|
||||
{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-black tabular-nums leading-none', i === 0 ? 'text-brand' : 'text-ink')}>
|
||||
{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.35, delay: 0.24, ease: EASE_OUT }}
|
||||
transition={{ duration: 0.35, delay: 0.16, ease: EASE_OUT }}
|
||||
className="text-lg sm:text-xl text-text-secondary max-w-2xl leading-relaxed mb-10"
|
||||
>
|
||||
{subheading}
|
||||
@@ -233,7 +212,7 @@ function HeroSection({ heroData, stats }: {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, delay: 0.3, ease: EASE_OUT }}
|
||||
transition={{ duration: 0.35, delay: 0.24, ease: EASE_OUT }}
|
||||
className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6"
|
||||
>
|
||||
<CTAButton href={ctaHref} dataTestId="hero-primary-cta">{ctaLabel}</CTAButton>
|
||||
@@ -254,7 +233,7 @@ function HeroSection({ heroData, stats }: {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.45, delay: 0.36, ease: EASE_OUT }}
|
||||
transition={{ duration: 0.45, delay: 0.3, ease: EASE_OUT }}
|
||||
className="relative mt-16 lg:mt-0"
|
||||
>
|
||||
<HeroProductVisual className="max-w-xl w-full lg:ml-auto" />
|
||||
@@ -265,6 +244,42 @@ function HeroSection({ heroData, stats }: {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 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-bold tabular-nums leading-none',
|
||||
i === 0 ? 'text-brand' : 'text-ink'
|
||||
)}>
|
||||
{stat.value}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 信任/成果区:可验证信任信号 + 首批客户共创标签 =====
|
||||
// 共创计划默认状态条目(如实呈现,无真实案例时不虚构客户)
|
||||
const DEFAULT_EARLY_ACCESS_STATUS = [
|
||||
@@ -968,8 +983,11 @@ export default function HomeContentV15({ services, cases, stats, heroData, news,
|
||||
}) {
|
||||
return (
|
||||
<main>
|
||||
{/* 1. Hero:全出血深色画布 + 品牌红单电压(对标 Accenture 纯黑 Hero) */}
|
||||
<HeroSection heroData={heroData} stats={stats} />
|
||||
{/* 1. Hero:浅色 editorial 画布 + 品牌红单电压(参考 Accenture Radical Relevance) */}
|
||||
<HeroSection heroData={heroData} />
|
||||
|
||||
{/* 1.5 成果带:数据指标下沉,滚动触发(Glance 层,声明先行的承接) */}
|
||||
<HeroStatsBand stats={stats} />
|
||||
|
||||
{/* 2. 信任区:量化数据成果展示 */}
|
||||
<TrustSection pageCopy={pageCopy} />
|
||||
|
||||
@@ -41,7 +41,7 @@ export function HeroProductVisual({ className }: { className?: string }) {
|
||||
<span className="text-[11px] font-medium text-text-muted">{kpi.label}</span>
|
||||
<TrendingUp className="h-3.5 w-3.5 text-brand" />
|
||||
</div>
|
||||
<div className="text-xl font-black tabular-nums leading-none text-ink sm:text-2xl">
|
||||
<div className="text-xl font-bold tabular-nums leading-none text-ink sm:text-2xl">
|
||||
{kpi.value}
|
||||
</div>
|
||||
<TrendBadge trend={kpi.trend} label={kpi.note} />
|
||||
|
||||
Reference in New Issue
Block a user