feat(ui): 抽离 CTAButton 为 src/components/ui/cta-button.tsx - 4 variant 覆盖全站 CTA 场景: * primary:品牌红填充 pill(hero 主行动 / 底部大 CTA) * secondary:描边墨色 pill(浅色 section 次行动) * dark:描边浅色 pill(深色区块 bg-dark-bg 内次行动,如 brand/team 底部 CTA) * inline:透明 + 小箭头(卡片内次行动) - 品牌箭头签名化:内置 ArrowRight + hover translate-x 微交互 - 200-300ms transition,对齐项目动效四原则 - data-testid 透传避免破坏既有测试 style(cta): P5 全站 CTA 签名切换(17 页面 27 处) - home V15 / news×3 / products×4(含 erp)/ cases / services×2 / solutions×2 - brand / methodology / team(hero 2 + L4 2 = 4 处,含 dark variant) - error / not-found(保留 onClick 功能按钮与卡片式导航 StaticLink) - 清理:StaticLink / Button / ArrowRight / ChevronRight unused import test(marketing): services/solutions lucide-react mock 补 ArrowRight - CTAButton 内部用 ArrowRight,原 mock 漏导导致渲染崩(4 测试失败已修) - 对齐 home-content-v15.test.tsx 既有 mock 范式
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { ArrowRight } from 'lucide-react';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
/**
|
||
* CTAButton — 全站统一的「行动召唤」组件
|
||
*
|
||
* 设计意图(见 .impeccable.md / Phase B P5):
|
||
* - Pill 化(rounded-full),与项目 Button 组件 size 体系一致
|
||
* - 品牌箭头签名化:内置 ArrowRight,hover 时 translate-x 形成微交互
|
||
* - 四变体覆盖全站 CTA 场景,避免"全是大红 pill"的 UI 噪音:
|
||
* - primary:品牌红填充 pill + 大箭头(hero 主行动 / 底部大 CTA)
|
||
* - secondary:描边墨色 pill + 大箭头(浅色 section 次行动 / 跳转按钮)
|
||
* - dark:描边浅色 pill + 大箭头(深色区块(bg-dark-bg)内的次行动,如 brand/team 底部 CTA)
|
||
* - inline:透明 + 小箭头 + 文字色(卡片内"了解更多"轻量次行动)
|
||
*
|
||
* dark 变体于 2026-08-31 P5 批次 4 新增:brand/team 底部深色 CTA 区块的次行动此前用
|
||
* text-dark-text-primary + border-dark-border 手写描边,现统一进组件,避免深色背景上误用
|
||
* secondary(text-ink)导致不可见。
|
||
* - 200-300ms transition(动效四原则区间内)
|
||
* - 主行动(primary)触达品牌红 #C41E3A 单电压,遵循"每页 ≥ 3 处品牌红触达点,面积 ≤ 10%"约束
|
||
*
|
||
* 抽离自 src/app/(marketing)/home-content-v15.tsx (2026-08-31),原 primary/secondary 行为完全保留;
|
||
* 新增 inline 变体支持次行动场景;data-testid 透传避免破坏既有测试快照。
|
||
*/
|
||
export type CTAButtonVariant = 'primary' | 'secondary' | 'dark' | 'inline';
|
||
|
||
export interface CTAButtonProps {
|
||
children: React.ReactNode;
|
||
href: string;
|
||
variant?: CTAButtonVariant;
|
||
className?: string;
|
||
dataTestId?: string;
|
||
/** 内部 span 的额外类名(用于覆盖默认间距/字号等极端定制场景) */
|
||
innerClassName?: string;
|
||
}
|
||
|
||
const VARIANT_STYLES: Record<CTAButtonVariant, string> = {
|
||
primary:
|
||
'bg-brand text-white hover:bg-brand-hover px-10 py-4 font-semibold text-base',
|
||
secondary:
|
||
'text-ink border border-ink/20 hover:border-ink/40 hover:bg-ink/5 px-10 py-4 font-semibold text-base',
|
||
dark:
|
||
'text-dark-text-primary border border-dark-border hover:border-dark-text-muted hover:bg-white/5 px-10 py-4 font-semibold text-base',
|
||
inline:
|
||
'text-text-primary hover:text-brand px-0 py-0 font-medium text-sm bg-transparent border-0',
|
||
};
|
||
|
||
const ARROW_STYLES: Record<CTAButtonVariant, string> = {
|
||
primary: 'w-5 h-5 transition-transform duration-300 group-hover:translate-x-2',
|
||
secondary: 'w-5 h-5 transition-transform duration-300 group-hover:translate-x-2',
|
||
dark: 'w-5 h-5 transition-transform duration-300 group-hover:translate-x-2',
|
||
inline: 'w-4 h-4 transition-transform duration-300 group-hover:translate-x-1',
|
||
};
|
||
|
||
export function CTAButton({
|
||
children,
|
||
href,
|
||
variant = 'primary',
|
||
className,
|
||
dataTestId,
|
||
innerClassName,
|
||
}: CTAButtonProps) {
|
||
const isPill = variant !== 'inline';
|
||
return (
|
||
<a
|
||
href={href}
|
||
data-testid={dataTestId}
|
||
className={cn(
|
||
'group inline-flex items-center gap-3 transition-all duration-200',
|
||
isPill ? 'rounded-full' : '',
|
||
VARIANT_STYLES[variant],
|
||
className
|
||
)}
|
||
>
|
||
<span className={cn(innerClassName)}>{children}</span>
|
||
<ArrowRight className={ARROW_STYLES[variant]} />
|
||
</a>
|
||
);
|
||
}
|
||
|
||
export default CTAButton; |