feat: 五站五种登录页设计(去 shadcn 默认味)+ 语言路由确认

- FormShell 重写为六种布局变体,五站各用一种:
  · AI Stack  = glass      深蓝渐变全屏 + 玻璃拟态浮动卡片
  · LaunchAI  = gradient   橙色渐变全屏 + 白色大圆角卡片
  · The AI Ledger = editorial 报纸头版:双细线报头(日期/品牌/标语衬线居中)
                            + 下划线输入 + 方角大写 tracking 黑按钮
  · DevAI.tools = terminal 深色终端窗口(三色标题栏 + $ devai.tools auth --login)
                            + 等宽输入 + 绿底黑字命令按钮
  · findai    = minimal    纯白居中 + 大圆角浅灰输入 + 紫色按钮
- 输入框/标签/社交按钮按 data-theme 形态变化(auth 路由注入 data-theme)
- 修复 FormShell 各变体根元素缺 w-full(被 auth flex 容器挤压成 1/3 宽)
- 回归:五站构建 OK / mock 登录 OK
This commit is contained in:
AI Directory Platform
2026-09-09 20:06:10 +00:00
parent 6baf0246d2
commit 3fddde470b
19 changed files with 331 additions and 79 deletions
@@ -109,6 +109,7 @@ export function LoginForm() {
isLoading={isLoading}
/>
}
variant={(process.env.NEXT_PUBLIC_AUTH_VARIANT as "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split" | undefined) ?? "split"}
brandName={process.env.NEXT_PUBLIC_APP_NAME}
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
@@ -127,6 +127,7 @@ export function SignupForm() {
isLoading={isLoading}
/>
}
variant={(process.env.NEXT_PUBLIC_AUTH_VARIANT as "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split" | undefined) ?? "split"}
brandName={process.env.NEXT_PUBLIC_APP_NAME}
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
@@ -6,7 +6,6 @@ import { useTranslations } from 'next-intl';
import { Form } from "@/components/ui/form";
import { Logo } from "@/components/Logo";
import { Button } from "@/components/button";
import { cn } from "@/lib/utils";
interface FormShellProps<TFieldValues extends FieldValues> {
@@ -23,14 +22,26 @@ interface FormShellProps<TFieldValues extends FieldValues> {
className?: string;
headerSlot?: React.ReactNode;
socialSlot?: React.ReactNode;
/** 站点品牌名(默认取环境变量,五站登录页各不相同) */
/** 站点品牌名 */
brandName?: string;
/** 品牌侧栏标语 */
brandTagline?: string;
/** 品牌强调色 HSL 三元组(如 "16 85% 55%"),传入即启用分栏品牌模板 */
/** 品牌强调色 HSL 三元组 */
accent?: string;
/** 登录布局变体:五种站点五种形态 */
variant?: "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split";
}
/**
* # 组件 FormShell
* 说明:认证表单外壳 —— 六种布局形态,五个网站各用一种:
* - glass 深蓝渐变全屏 + 玻璃拟态浮动卡片(AI Stack)
* - gradient 品牌色渐变全屏 + 白色大圆角卡片(LaunchAI)
* - editorial 报纸头版:衬线居中报头 + 下划线输入 + 方角黑按钮(The AI Ledger)
* - terminal 深色终端窗口 + 等宽输入 + 命令行按钮(DevAI.tools)
* - minimal 纯白极简居中 + 大圆角搜索式输入(findai)
* - split 经典左右分栏(后备)
*/
export function FormShell<TFieldValues extends FieldValues>({
form,
title,
@@ -48,99 +59,231 @@ export function FormShell<TFieldValues extends FieldValues>({
brandName,
brandTagline,
accent,
variant,
}: FormShellProps<TFieldValues>) {
const t = useTranslations('auth.social');
const brand = brandName || process.env.NEXT_PUBLIC_APP_NAME || 'AI Directory';
// 品牌模式用站点品牌字标(不传 headerSlot 时),非品牌模式保留原 Logo
const headerContent =
headerSlot !== undefined
? headerSlot
: accent
? <span className="inline-flex items-center gap-2 text-lg font-bold text-foreground">
<span className="flex h-8 w-8 items-center justify-center rounded-lg text-white" style={{ backgroundColor: `hsl(${accent})` }}>
{brand.slice(0, 1)}
</span>
{brand}
</span>
: <Logo />;
const tagline = brandTagline || `Welcome back to ${brand}.`;
const accentHsl = accent ? `hsl(${accent})` : 'hsl(222 47% 11%)';
const initial = brand.slice(0, 1);
// 品牌模板:传入 accent 时渲染分栏布局(左侧品牌区 / 右侧表单),每站视觉不同
const formColumn = (
<div className="mx-auto w-full max-w-md">
<div>
<div className="flex">{headerContent}</div>
<h2 className="mt-8 text-3xl font-bold leading-9 tracking-tight text-foreground">
{title}
</h2>
{description ? (
<p className="mt-2 text-[0.95rem] leading-6 text-muted-foreground">{description}</p>
) : null}
</div>
<div className="mt-8">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{socialSlot}
{socialSlot ? (
<div className="flex items-center gap-3">
<span className="h-px flex-1 bg-border" />
<span className="text-xs text-muted-foreground">{t('orContinueWith')}</span>
<span className="h-px flex-1 bg-border" />
</div>
) : null}
{children}
{error ? (
<div className="rounded-md bg-destructive/10 p-3 text-[0.95rem] text-red-600">{error}</div>
) : null}
<Button className="w-full" disabled={isLoading} type="submit">
{isLoading ? submitLoadingText ?? submitText : submitText}
</Button>
</form>
{footer}
</div>
const errorBox = error ? (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-red-600" role="alert">
{error}
</div>
) : null;
const submitBtn = (cls: string) => (
<button type="submit" disabled={isLoading} className={cls}>
{isLoading ? submitLoadingText ?? submitText : submitText}
</button>
);
if (accent) {
const footerNode = footer ? <div className="mt-6">{footer}</div> : null;
/* ================= 1. AI Stack:深蓝渐变 + 玻璃卡片 ================= */
if (variant === "glass") {
return (
<Form {...form}>
{/* 品牌分栏模板:col-span 全宽(覆盖 AuthLayout 的 2 列网格),左品牌右表单 */}
<div className={cn("col-span-full grid min-h-screen w-full lg:grid-cols-2", className)}>
<div
className="relative hidden flex-col justify-between overflow-hidden p-12 lg:flex"
style={{ backgroundColor: `hsl(${accent})` }}
>
<div className="flex items-center gap-2 text-xl font-bold text-white">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 text-lg">
{brand.slice(0, 1)}
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-[linear-gradient(160deg,hsl(224_60%_12%),hsl(224_55%_22%)_60%,hsl(210_45%_30%))] px-4 py-12">
<div className="pointer-events-none absolute -left-40 -top-40 h-96 w-96 rounded-full bg-blue-400/10 blur-3xl" />
<div className="pointer-events-none absolute -bottom-40 -right-40 h-96 w-96 rounded-full bg-indigo-400/10 blur-3xl" />
<div className="relative w-full max-w-md rounded-2xl border border-white/15 bg-white/[0.08] p-8 shadow-2xl backdrop-blur-xl sm:p-10">
<div className="mb-8 flex items-center gap-2.5">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 text-lg font-bold text-white">
{initial}
</span>
{brand}
<span className="text-lg font-bold text-white">{brand}</span>
</div>
<div>
<p className="max-w-sm text-2xl font-semibold leading-snug text-white">
{brandTagline || `Welcome back to ${brand}.`}
</p>
<p className="mt-3 max-w-sm text-sm leading-relaxed text-white/70">
Vote, review and save the AI tools you love — join the community.
</p>
</div>
<p className="text-xs text-white/50">© {new Date().getFullYear()} {brand}</p>
</div>
<div className="flex items-center justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
{formColumn}
<h1 className="text-2xl font-bold text-white">{title}</h1>
{description && <p className="mt-2 text-sm text-white/60">{description}</p>}
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-5">
{socialSlot}
{children}
{errorBox}
{submitBtn("w-full rounded-xl bg-white py-3 text-sm font-semibold text-slate-900 transition hover:bg-white/90 disabled:opacity-60")}
</form>
{footerNode}
</div>
</div>
</Form>
);
}
/* ================= 2. LaunchAI:品牌渐变 + 白色大圆角卡片 ================= */
if (variant === "gradient") {
return (
<Form {...form}>
<div
className="relative flex min-h-screen w-full items-center justify-center px-4 py-12"
style={{ background: `linear-gradient(140deg, hsl(${accent}) 0%, hsl(28 90% 55%) 100%)` }}
>
<div className="w-full max-w-md rounded-3xl bg-white p-8 shadow-[0_24px_70px_rgba(0,0,0,0.25)] sm:p-10">
<div className="mb-7 flex items-center gap-2.5">
<span
className="flex h-10 w-10 items-center justify-center rounded-2xl text-lg font-bold text-white"
style={{ backgroundColor: accentHsl }}
>
{initial}
</span>
<span className="text-xl font-extrabold tracking-tight text-foreground">{brand}</span>
</div>
<h1 className="text-3xl font-extrabold tracking-tight text-foreground">{title}</h1>
{description && <p className="mt-2 text-sm text-muted-foreground">{description}</p>}
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-7 space-y-5">
{socialSlot}
{children}
{errorBox}
{submitBtn(`w-full rounded-2xl py-3.5 text-base font-bold text-white transition hover:opacity-90 disabled:opacity-60`)}
</form>
{footerNode}
</div>
</div>
</Form>
);
}
/* ================= 3. The AI Ledger:报纸头版 + 下划线输入 ================= */
if (variant === "editorial") {
const lineInput =
"w-full border-0 border-b-2 border-neutral-300 bg-transparent px-0 py-2 text-lg font-serif text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-900 focus:outline-none rounded-none";
return (
<Form {...form}>
<div className="min-h-screen w-full bg-[hsl(42_45%_97%)] px-4 py-10 font-serif text-neutral-900">
<div className="mx-auto max-w-xl">
{/* 报头 */}
<div className="border-y-[3px] border-double border-neutral-900 py-3 text-center">
<p className="text-[10px] uppercase tracking-[0.35em] text-neutral-500">
{new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" })}
</p>
<h1 className="mt-1 text-4xl font-black tracking-tight">{brand}</h1>
<p className="mt-1 text-[10px] uppercase tracking-[0.35em] text-neutral-500">{tagline}</p>
</div>
<h2 className="mt-10 text-center text-2xl font-bold">{title}</h2>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-6">
{socialSlot}
{children}
{errorBox}
{submitBtn("w-full border-2 border-neutral-900 bg-neutral-900 py-3 text-sm font-bold uppercase tracking-[0.2em] text-white transition hover:bg-transparent hover:text-neutral-900 disabled:opacity-60")}
</form>
{footerNode}
</div>
<div className="mx-auto mt-12 max-w-xl border-t border-neutral-300 pt-4 text-center text-[10px] uppercase tracking-[0.3em] text-neutral-400">
© {new Date().getFullYear()} {brand}
</div>
</div>
</Form>
);
}
/* ================= 4. DevAI.tools:深色终端窗口 ================= */
if (variant === "terminal") {
const termInput =
"w-full rounded border border-emerald-500/30 bg-black/50 px-3 py-2.5 font-mono text-sm text-emerald-100 placeholder:text-emerald-100/30 focus:border-emerald-400/70 focus:outline-none rounded-md";
const termLabel = "mb-1 block font-mono text-xs text-emerald-500/80";
return (
<Form {...form}>
<div className="flex min-h-screen w-full items-center justify-center bg-[hsl(220_25%_7%)] px-4 py-12">
<div className="w-full max-w-lg overflow-hidden rounded-lg border border-emerald-500/20 bg-[hsl(220_25%_9%)] shadow-[0_0_40px_rgba(16,185,129,0.06)]">
{/* 终端标题栏 */}
<div className="flex items-center gap-2 border-b border-emerald-500/15 bg-black/40 px-4 py-3">
<span className="h-3 w-3 rounded-full bg-red-500/80" />
<span className="h-3 w-3 rounded-full bg-yellow-500/80" />
<span className="h-3 w-3 rounded-full bg-emerald-500/80" />
<span className="ml-3 font-mono text-xs text-emerald-100/50">{brand.toLowerCase()} — auth</span>
</div>
<div className="p-7">
<p className="font-mono text-sm text-emerald-400/90">
<span className="text-emerald-600">$</span> {brand.toLowerCase()} auth --login
</p>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-6 space-y-5">
{socialSlot}
{children}
{errorBox}
{submitBtn("w-full rounded-md bg-emerald-500 py-3 font-mono text-sm font-bold text-black transition hover:bg-emerald-400 disabled:opacity-60")}
</form>
{footerNode}
</div>
</div>
</div>
</Form>
);
}
/* ================= 5. findai:纯白极简居中 ================= */
if (variant === "minimal") {
const softInput =
"w-full rounded-2xl border border-transparent bg-neutral-100 px-5 py-4 text-base text-foreground placeholder:text-neutral-400 focus:border-violet-300 focus:bg-white focus:outline-none focus:ring-4 focus:ring-violet-500/10 transition rounded-2xl";
return (
<Form {...form}>
<div className="flex min-h-screen w-full flex-col items-center justify-center bg-white px-4 py-12">
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-600 text-xl font-bold text-white">
{initial}
</span>
<h1 className="mt-6 text-3xl font-bold tracking-tight text-foreground">{brand}</h1>
<p className="mt-2 text-sm text-neutral-400">{tagline}</p>
<div className="mt-10 w-full max-w-sm">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{socialSlot}
{children}
{errorBox}
{submitBtn("w-full rounded-2xl bg-violet-600 py-4 text-base font-semibold text-white transition hover:bg-violet-700 disabled:opacity-60")}
</form>
{footerNode}
</div>
<p className="mt-16 text-xs text-neutral-300">{brand} — search, ranked by people</p>
</div>
</Form>
);
}
/* ================= 后备:左右分栏 ================= */
return (
<Form {...form}>
<div
className={cn(
"flex w-full items-center justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24",
"grid min-h-screen w-full lg:grid-cols-2",
className
)}
>
{formColumn}
<div
className="relative hidden flex-col justify-between overflow-hidden p-12 lg:flex"
style={{ backgroundColor: accentHsl }}
>
<div className="flex items-center gap-2 text-xl font-bold text-white">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 text-lg">
{initial}
</span>
{brand}
</div>
<div>
<p className="max-w-sm text-2xl font-semibold leading-snug text-white">{tagline}</p>
<p className="mt-3 max-w-sm text-sm leading-relaxed text-white/70">
Vote, review and save the AI tools you love — join the community.
</p>
</div>
<p className="text-xs text-white/50">© {new Date().getFullYear()} {brand}</p>
</div>
<div className="flex items-center justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-md">
<span className="inline-flex items-center gap-2 text-lg font-bold text-foreground">
<span className="flex h-8 w-8 items-center justify-center rounded-lg text-white" style={{ backgroundColor: accentHsl }}>
{initial}
</span>
{brand}
</span>
<h2 className="mt-8 text-3xl font-bold tracking-tight text-foreground">{title}</h2>
{description && <p className="mt-2 text-[0.95rem] text-muted-foreground">{description}</p>}
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-5">
{socialSlot}
{children}
{errorBox}
{submitBtn("w-full rounded-xl bg-primary py-3 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-60")}
</form>
{footerNode}
</div>
</div>
</div>
</Form>
);
@@ -23,7 +23,7 @@ type FormTextFieldProps<TFieldValues extends FieldValues> = BaseProps &
};
const inputClasses =
"block w-full rounded-md border-0 bg-input px-4 py-1.5 text-foreground shadow-aceternity placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring";
"auth-input block w-full rounded-md border-0 bg-input px-4 py-2.5 text-[0.95rem] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-colors";
export function FormTextField<TFieldValues extends FieldValues>({
control,
+2
View File
@@ -34,6 +34,8 @@ export interface SiteConfig {
accent?: string;
/** 站点视觉签名 class(挂载在站点外壳根元素,驱动全站差异化样式) */
themeClass?: string;
/** 登录页布局变体:五站五种形态 */
authVariant?: "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split";
/** 中文品牌文案(导航/页脚/hero/搜索占位) */
brandZh?: {
name?: string;
+17 -5
View File
@@ -1,15 +1,27 @@
"use client";
import { cn } from "@/lib/utils";
/**
* # 组件 AuthLayout
* 说明:认证页容器。布局职责已下沉到 FormShell(品牌分栏/居中两套模板),
* 这里只提供页面底色,避免双层 grid 叠加导致的多列错位。
* 说明:认证页容器。布局职责在 FormShell(六种布局变体);
* 这里注入 data-theme(由 NEXT_PUBLIC_AUTH_VARIANT 映射),
* 使输入框/标签/按钮的站点主题化样式(globals.css)在 auth 路由组生效。
*/
const VARIANT_THEME: Record<string, string> = {
glass: "ai-stack",
gradient: "launchai",
editorial: "the-ai-ledger",
terminal: "devai-tools",
minimal: "findai",
split: "ai-stack",
};
export function AuthLayout({ children }: { children: React.ReactNode }) {
const variant = process.env.NEXT_PUBLIC_AUTH_VARIANT || "glass";
return (
<div className={cn("w-full min-h-screen bg-background")}>
<div
data-theme={VARIANT_THEME[variant] ?? "ai-stack"}
className="min-h-screen w-full"
>
{children}
</div>
);
+77
View File
@@ -383,3 +383,80 @@ html {
background: hsl(var(--background));
clip-path: polygon(0 100%, 100% 0, 100% 100%, 0 100%);
}
/* ============================================================
认证页主题化:输入框 / 标签 / 社交按钮按站点形态变化
============================================================ */
/* The AI Ledger:下划线输入 + 衬线 + 方角按钮 */
[data-theme="the-ai-ledger"] .auth-input {
border-radius: 0;
border: 0;
border-bottom: 2px solid hsl(24 10% 70%);
background: transparent;
font-family: Georgia, "Times New Roman", serif;
font-size: 1.05rem;
padding-left: 0;
padding-right: 0;
}
[data-theme="the-ai-ledger"] .auth-input:focus {
border-bottom-color: hsl(24 10% 12%);
box-shadow: none;
}
[data-theme="the-ai-ledger"] form button[type="submit"],
[data-theme="the-ai-ledger"] form .rounded-lg {
border-radius: 1px;
}
[data-theme="the-ai-ledger"] form label {
font-family: Georgia, serif;
letter-spacing: 0.02em;
}
/* DevAI.tools:终端等宽输入 */
[data-theme="devai-tools"] .auth-input {
border-radius: 0.375rem;
border: 1px solid hsl(150 60% 45% / 0.3);
background: rgba(0, 0, 0, 0.45);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
color: hsl(150 40% 88%);
}
[data-theme="devai-tools"] .auth-input::placeholder {
color: hsl(150 30% 50% / 0.4);
}
[data-theme="devai-tools"] .auth-input:focus {
border-color: hsl(150 60% 45% / 0.7);
box-shadow: 0 0 0 3px hsl(150 60% 45% / 0.1);
}
[data-theme="devai-tools"] form label {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.75rem;
color: hsl(150 40% 60% / 0.85);
}
[data-theme="devai-tools"] form .rounded-lg {
border-radius: 0.375rem;
border-color: hsl(150 60% 45% / 0.25);
background: rgba(0, 0, 0, 0.3);
}
/* findai:大圆角浅灰底输入 */
[data-theme="findai"] .auth-input {
border-radius: 1rem;
background: hsl(0 0% 96%);
}
[data-theme="findai"] .auth-input:focus {
background: white;
box-shadow: 0 0 0 4px hsl(262 65% 52% / 0.1);
}
[data-theme="findai"] form .rounded-lg {
border-radius: 1rem;
}
/* LaunchAI:大圆角输入 */
[data-theme="launchai"] .auth-input {
border-radius: 0.875rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
[data-theme="launchai"] form .rounded-lg {
border-radius: 0.875rem;
}