首页
This commit is contained in:
172
src/app/(home)/components/home/ui.tsx
Normal file
172
src/app/(home)/components/home/ui.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import Link from "next/link";
|
||||
import {CheckCircle2} from "lucide-react";
|
||||
|
||||
import {
|
||||
IconCardProps,
|
||||
MetricContent, PricingTierContent,
|
||||
SectionIntroContent, WorkflowStepContent,
|
||||
} from "./types";
|
||||
|
||||
/** 首页区块上方的小号眉标文字。 */
|
||||
export function Eyebrow({children}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页复用的区块标题结构。
|
||||
* 多个区块共享同一套标题层级,集中在这里可以避免字号和间距漂移。
|
||||
*/
|
||||
export function SectionIntro({
|
||||
intro,
|
||||
align = "center",
|
||||
}: {
|
||||
intro: SectionIntroContent;
|
||||
align?: "center" | "left";
|
||||
}) {
|
||||
const wrapperClassName =
|
||||
align === "center" ? "mx-auto max-w-2xl text-center" : "max-w-2xl text-left";
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
{intro.eyebrow && <Eyebrow>{intro.eyebrow}</Eyebrow>}
|
||||
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight md:text-4xl">
|
||||
{intro.title}
|
||||
</h2>
|
||||
{intro.body && (
|
||||
<p className="mt-4 text-balance text-base text-muted-foreground">{intro.body}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 数据条中的单个指标展示。 */
|
||||
export function MetricStat({metric}: { metric: MetricContent }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-3xl font-semibold tracking-tight tabular-nums md:text-4xl">
|
||||
{metric.number}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{metric.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 首页复用的图标说明卡,支持问题区和方案区两种密度。 */
|
||||
export function IconCard({Icon, title, body, variant = "default"}: IconCardProps) {
|
||||
const iconSize = variant === "problem" ? "md" : "sm";
|
||||
const bodyClassName =
|
||||
variant === "problem"
|
||||
? "mt-3 text-sm leading-relaxed text-muted-foreground"
|
||||
: "mt-2 text-sm leading-relaxed text-muted-foreground";
|
||||
|
||||
return (
|
||||
<div className="marketing-lift h-full rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<IconBubble Icon={Icon} size={iconSize}/>
|
||||
<h3 className="mt-4 text-base font-semibold tracking-tight">{title}</h3>
|
||||
<p className={bodyClassName}>{body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 工作流程区块的单个步骤卡片。 */
|
||||
export function WorkflowStepCard({step}: { step: WorkflowStepContent }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-foreground bg-card font-semibold tabular-nums">
|
||||
{step.number}
|
||||
</div>
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{step.eyebrow}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<IconBubble Icon={step.Icon} size="sm"/>
|
||||
</div>
|
||||
<h3 className="mt-3 text-base font-semibold tracking-tight">{step.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{step.body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 定价区的单个套餐卡片,包含高亮态和转化入口。 */
|
||||
export function PricingTierCard({tier}: { tier: PricingTierContent }) {
|
||||
const TierIcon = tier.Icon;
|
||||
const cardClassName = tier.isHighlighted
|
||||
? "marketing-lift relative flex h-full flex-col rounded-2xl border-2 border-foreground bg-card p-7 shadow-lg"
|
||||
: "marketing-lift relative flex h-full flex-col rounded-2xl border border-border bg-card p-7 shadow-sm";
|
||||
const ctaClassName = tier.isHighlighted
|
||||
? "mt-6 inline-flex h-10 w-full items-center justify-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition-opacity hover:opacity-90"
|
||||
: "mt-6 inline-flex h-10 w-full items-center justify-center rounded-md border border-border bg-background px-5 text-sm font-medium transition-colors hover:bg-accent";
|
||||
|
||||
return (
|
||||
<div className={cardClassName}>
|
||||
{tier.badge && (
|
||||
<div
|
||||
className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-foreground px-3 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-background">
|
||||
{tier.badge}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<TierIcon className="h-4 w-4 text-foreground/80" aria-hidden/>
|
||||
<span className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{tier.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-end gap-2">
|
||||
<span className="text-4xl font-semibold tracking-tight">{tier.price}</span>
|
||||
{tier.unit && <span className="pb-1 text-sm text-muted-foreground">{tier.unit}</span>}
|
||||
</div>
|
||||
{tier.note && <p className="mt-2 text-[11px] text-muted-foreground">{tier.note}</p>}
|
||||
<ul className="mt-5 flex-1 space-y-2.5">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-foreground/80" aria-hidden/>
|
||||
<span className="text-foreground/85">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link href={tier.cta.href} className={ctaClassName}>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 常见问题区块的折叠问答项。 */
|
||||
export function FaqDisclosure({question, answer}: { question: string; answer: string }) {
|
||||
return (
|
||||
<details className="group rounded-md border border-border bg-card p-5 shadow-sm">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 text-sm font-medium">
|
||||
<span>{question}</span>
|
||||
<span
|
||||
className="text-muted-foreground transition-transform group-open:rotate-180"
|
||||
aria-hidden="true"
|
||||
>
|
||||
v
|
||||
</span>
|
||||
</summary>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">{answer}</p>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片内部统一的小图标容器,保证所有图标尺寸和底色一致。 */
|
||||
function IconBubble({Icon, size}: { Icon: IconCardProps["Icon"]; size: "sm" | "md" }) {
|
||||
const boxClassName =
|
||||
size === "md"
|
||||
? "inline-flex h-10 w-10 items-center justify-center rounded-md bg-foreground/[0.04] text-foreground/80 ring-1 ring-foreground/[0.06]"
|
||||
: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-foreground/[0.04] text-foreground/80 ring-1 ring-foreground/[0.06]";
|
||||
const iconClassName = size === "md" ? "h-5 w-5" : "h-[18px] w-[18px]";
|
||||
|
||||
return (
|
||||
<div className={boxClassName}>
|
||||
<Icon className={iconClassName} aria-hidden/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user