Files
novalon-website/src/components/ui/cta-button.tsx
T
zhangxiang 5c1b883abf feat(ui): CTAButton 可复用组件 + P5 全站 CTA 签名切换(17 页面 27 处)
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 范式
2026-08-31 17:25:08 +08:00

84 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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;