Files
store_ai_front/src/app/(home)/components/home/ui.tsx
2026-05-07 16:14:31 +08:00

173 lines
7.3 KiB
TypeScript

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>
);
}