跳至正文
区块

Split Sign-in分屏登录页

左侧品牌面板 + 右侧表单的分屏登录页:登录(密码 / 验证码)、注册(带强度条)、找回密码,zod 校验;品牌面板与表单控件直接用本站自己的组件拼成。

正在加载…

Auth01 提炼自我的一个企业监控平台项目的登录页,在原有「左侧品牌面板 + 右侧表单」结构上,补上了完整的账号流程:

  • 登录:密码登录 / 验证码登录用方条分段控件切换;账号(用户名或邮箱)、密码显示切换、记住登录状态开关、忘记密码、第三方登录(GitHub、Google)、跳转注册。
  • 注册:姓名、邮箱、密码(实时强度条)、确认密码、同意服务条款。
  • 找回密码:邮箱提交后切换为「邮件已发送」的成功状态,邮件图标向外扩散波纹。
  • 三个视图在同一个组件内切换,带 180ms 的淡入位移过渡,系统开启「减少动态效果」时只保留淡入。
  • 大屏左半屏是品牌面板,窄屏隐藏面板并在表单上方显示 Logo 与名称。
  • 品牌面板的颜色都来自主题变量,切换颜色主题和亮暗模式后自动跟随:亮色下是 primary 底配 primary-foreground 文字;暗色下 primary 会变成浅色,面板改用 card 深底,只叠一层低透明度的 primary 渐变染上主题色,文字回到 foreground。光斑、波纹、字符矩阵共用一个 --panel-glow 变量。预览里可以直接试。
  • 输入框、提交按钮、第三方登录按钮、「获取验证码」、「返回登录」统一为 36px(按钮用 size="lg",输入框 h-9),与方条分段控件的 lg 档对齐。

用到的本站组件

这个区块本身就是自研组件的一组真实用例,除表单输入、按钮、复选框(注册页的条款勾选)仍用 shadcn 原语外,其余都换成了本站组件:

位置组件说明
品牌面板背景ripple以面板中心向外扩散的同心波纹,替代原来的静态圆环
品牌面板四周glyph-field淡淡的 0 / 1 字符矩阵,fade="center" 让中间留白给文字
品牌面板标题text-animate 的 TextReveal挂载时按词逐个显现,读屏仍读完整标题
登录方式切换segmented-control密码登录 / 验证码登录,替代原来的 Tabs
记住登录状态toggle-switch即时生效的设置项,替代原来的复选框
「邮件已发送」ripple邮件图标为圆心,表达「已发出」

安装

pnpm dlx shadcn@latest add https://yiui.dev/r/auth-01.json
会连带安装 shadcn 组件 button、input、label、checkbox, 本站条目 grid-layout、flow-diagram、ripple、glyph-field、text-animate、segmented-control、toggle-switch。已存在的文件会询问是否覆盖。

会安装 shadcn 的 button、input、label、checkbox,本站的 ripple、glyph-field、text-animate、segmented-control、toggle-switch(及它们依赖的 flow-diagram 色调与背景),以及 npm 包 motion、zod、lucide-react。如果项目里已有同名文件,CLI 会询问是否覆盖。

用法

最简单的用法是直接渲染,不传任何属性。此时所有回调都退回「控制台打印 + 模拟 0.8 秒请求」的示例实现,方便先看效果:

app/login/page.tsx
import { Auth01 } from "@/components/auth-01/auth-01"

export default function LoginPage() {
  return <Auth01 />
}

接入你的认证服务

回调都返回 Promise。组件负责按钮的 loading 状态;失败时 throw new Error("中文原因"),错误信息会显示在表单上方。 下面以 better-auth 为例,NextAuth / Auth.js、Supabase 等同理:

app/login/page.tsx
"use client"

import { useRouter } from "next/navigation"
import { Auth01 } from "@/components/auth-01/auth-01"
import { authClient } from "@/lib/auth-client"

export default function LoginPage() {
  const router = useRouter()
  return (
    <Auth01
      brand={{ name: "你的产品", title: "一句话说清价值" }}
      onSignIn={async (values) => {
        if (values.method === "code") {
          // 验证码登录:values.phone / values.code
          throw new Error("暂未开通验证码登录")
        }
        const isEmail = values.account.includes("@")
        const { error } = isEmail
          ? await authClient.signIn.email({
              email: values.account,
              password: values.password,
              rememberMe: values.remember,
            })
          : await authClient.signIn.username({
              username: values.account,
              password: values.password,
              rememberMe: values.remember,
            })
        if (error) throw new Error(error.message ?? "登录失败")
        router.push("/")
      }}
      onSignUp={async ({ name, email, password }) => {
        const { error } = await authClient.signUp.email({ name, email, password })
        if (error) throw new Error(error.message ?? "注册失败")
        router.push("/")
      }}
      onForgotPassword={async (email) => {
        const { error } = await authClient.requestPasswordReset({ email })
        if (error) throw new Error(error.message ?? "发送失败")
      }}
      onSendCode={async (phone) => {
        await fetch("/api/sms/send", { method: "POST", body: JSON.stringify({ phone }) })
      }}
      onSocialSignIn={async (provider) => {
        await authClient.signIn.social({ provider })
      }}
    />
  )
}

使用 NextAuth 时,把 onSignIn 换成 signIn("credentials", { redirect: false, ... }) 并在返回 error 时抛出即可;第三方登录对应 signIn(provider)。

换成英文

labels 也能覆盖校验提示(validation* 键)。提示里的 {min}、{length} 由 schemas.ts 的规则常量填入,保留占位符就会显示成对应的数字:

app/login/page.tsx
import { Auth01, type Auth01Labels } from "@/components/auth-01/auth-01"

// 只覆盖需要改的键,没写的键仍是默认中文
const english: Partial<Auth01Labels> = {
  signInTitle: "Welcome back",
  signInDescription: "Sign in to continue",
  accountLabel: "Username or email",
  accountPlaceholder: "you@example.com",
  passwordLabel: "Password",
  passwordPlaceholder: "Enter your password",
  signInSubmit: "Sign in",
  signUpTitle: "Create an account",
  nameLabel: "Name",
  emailLabel: "Email",
  newPasswordPlaceholder: "At least 8 characters, letters and numbers",
  confirmPasswordLabel: "Confirm password",
  signUpSubmit: "Sign up",
  // 校验提示:{min}、{length} 一定要保留,否则数字不会被填入
  validationAccountRequired: "Enter your username or email",
  validationPasswordRequired: "Enter your password",
  validationEmailRequired: "Enter your email",
  validationEmailInvalid: "Enter a valid email address",
  validationPhoneInvalid: "Enter a valid {length}-digit phone number",
  validationCodeInvalid: "Enter the {length}-digit code",
  validationPasswordMin: "Use at least {min} characters",
  validationPasswordLetter: "Include a letter",
  validationPasswordDigit: "Include a number",
  validationNameMin: "Name needs at least {min} characters",
  validationConfirmRequired: "Re-enter your password",
  validationPasswordMismatch: "Passwords do not match",
  validationTermsRequired: "Please accept the terms to continue",
}

export default function LoginPage() {
  return <Auth01 labels={english} />
}

完整键与默认值见下方 labels。

API

Auth01

BrandConfig

labels

所有键都在 config.ts 的 DEFAULT_LABELS 里,传 labels={{ signInTitle: "Sign in" }} 这样覆盖其中任意几项。含 {name}、{email}、{seconds}、{level} 的文案会在渲染时填入实际值;表格末尾的 validation* 键是表单校验提示,其中的 {min}、{length} 由 schemas.ts 的规则常量填入(例如改小密码最小长度后,提示里的数字会自动变)。

键默认值说明
brandPanelLabel{name} 品牌介绍品牌面板的 aria-label
signInTitle欢迎回来登录视图标题(h1)
signInDescription输入账号信息即可登录登录视图说明
signInMethodLabel登录方式登录方式分段控件的 aria-label
signInPasswordTab密码登录密码登录选项
signInCodeTab验证码登录验证码登录选项
accountLabel用户名或邮箱账号输入框的标签
accountPlaceholder请输入用户名或邮箱账号输入框的占位文字
passwordLabel密码密码输入框的标签
passwordPlaceholder请输入密码密码输入框的占位文字
forgotPassword忘记密码?密码框右侧的链接按钮
remember记住登录状态记住登录状态开关的标签
signInSubmit登录登录按钮与其加载文案
signInPending登录中…登录按钮与其加载文案
phoneLabel手机号验证码登录的手机号输入框的标签
phonePlaceholder请输入 11 位手机号验证码登录的手机号输入框的占位文字
codeLabel验证码验证码输入框的标签
codePlaceholder6 位数字验证码验证码输入框的占位文字
sendCode获取验证码「获取验证码」按钮的三种状态
sendCodePending发送中…「获取验证码」按钮的三种状态
resendCode{seconds}s 后重发「获取验证码」按钮的三种状态
socialDivider或第三方登录上方的分隔文字
noAccount还没有账号?跳转注册
signUpLink注册跳转注册
signUpTitle创建账号注册视图标题(h1)
signUpDescription填写以下信息即可完成注册注册视图说明
nameLabel姓名姓名输入框的标签
namePlaceholder请输入姓名姓名输入框的占位文字
emailLabel邮箱邮箱输入框(注册与找回密码共用)
emailPlaceholdername@example.com邮箱输入框(注册与找回密码共用)
newPasswordPlaceholder至少 8 位,包含字母和数字注册密码框占位文字
confirmPasswordLabel确认密码确认密码输入框的标签
confirmPasswordPlaceholder再次输入密码确认密码输入框的占位文字
termsPrefix我已阅读并同意条款勾选行的四段文字
termsOfService服务条款条款勾选行的四段文字
termsConjunction和条款勾选行的四段文字
privacyPolicy隐私政策条款勾选行的四段文字
signUpSubmit注册注册按钮与其加载文案
signUpPending注册中…注册按钮与其加载文案
haveAccount已有账号?跳转登录
signInLink登录跳转登录
forgotPasswordTitle找回密码找回密码视图标题(h1)
forgotPasswordDescription输入注册时使用的邮箱,我们会发送重置链接找回密码视图说明
sendResetLink发送重置链接提交按钮与其加载文案
sendResetLinkPending发送中…提交按钮与其加载文案
backToSignIn返回登录返回登录按钮
emailSentTitle邮件已发送发送成功视图标题(h1)
emailSentDescription我们已向 {email} 发送了重置密码的链接,请查收邮件。没收到?请检查垃圾邮件箱。发送成功视图说明,{email} 会加粗显示
showPassword显示密码密码显示切换按钮的 aria-label
hidePassword隐藏密码密码显示切换按钮的 aria-label
passwordStrength强度:{level}强度条右侧文字
passwordStrengthEmpty强度:—强度条右侧文字
strengthWeak弱四个强度等级
strengthFair一般四个强度等级
strengthGood较强四个强度等级
strengthStrong强四个强度等级
validationAccountRequired请输入用户名或邮箱密码登录:账号为空
validationPasswordRequired请输入密码密码登录:密码为空
validationEmailRequired请输入邮箱邮箱为空(注册、找回密码)
validationEmailInvalid请输入有效的邮箱地址邮箱格式错误
validationPhoneInvalid请输入有效的 {length} 位手机号验证码登录:手机号格式错误,{length} 默认 11
validationCodeInvalid请输入 {length} 位数字验证码验证码格式错误,{length} 默认 6
validationPasswordMin密码至少 {min} 位密码太短,{min} 默认 8
validationPasswordLetter密码需要包含字母密码缺少字母
validationPasswordDigit密码需要包含数字密码缺少数字
validationNameMin姓名至少 {min} 个字符姓名太短,{min} 默认 2
validationConfirmRequired请再次输入密码确认密码为空
validationPasswordMismatch两次输入的密码不一致两次输入的密码不同
validationTermsRequired请先阅读并同意服务条款未勾选条款
requestFailed操作失败,请稍后重试回调抛出的错误没有 message 时的兜底提示

可访问性

  • 每个视图只有一个 h1(由 ViewHeading 渲染),品牌面板是带 aria-label 的 aside,光斑、波纹与字符矩阵等装饰全部 aria-hidden;品牌标题的逐词动画只作用在隐藏层,读屏读到的是完整原文。
  • 输入框与 Label 通过 htmlFor / id 关联;字段错误用 aria-describedby 关联到同一个输入框,出错时设置 aria-invalid;表单级错误是 role="alert",出现时立即朗读。
  • 密码显示切换按钮带 aria-pressed 和随状态变化的中文名称(显示密码 / 隐藏密码)。
  • 登录方式是带 aria-label 的单选组(segmented-control),方向键切换;「记住登录状态」是 role="switch" 的开关;「或」分隔线是 role="separator"。
  • 密码强度条是装饰(aria-hidden),文字结果用 aria-live="polite" 播报;强度不只靠颜色区分,同时有文字。
  • 「邮件已发送」视图是 role="status",读屏会读出提交结果。
  • 提交中按钮禁用并显示「登录中…」,避免重复提交;所有图标按钮都有中文 aria-label。
  • 视图切换动画遵循 prefers-reduced-motion,开启后只保留淡入;波纹、字符矩阵与标题显现在开启「减少动态效果」时显示静态画面,标题直接显示全文。

注意事项

文件结构

app/login/page.tsx                       # 页面入口:把回调接到你的认证服务
components/auth-01/
├── auth-01.tsx                          # 主组件:分屏布局 + 视图切换
├── config.ts                            # ★ 全部用户可见文案 DEFAULT_LABELS
├── brand.tsx                            # 品牌配置类型与默认示例品牌
├── brand-panel.tsx                      # 左侧品牌面板(波纹 + 字符矩阵 + 标题显现)、窄屏顶部品牌
├── sign-in-form.tsx                     # 登录(分段控件切换密码 / 验证码)+ 第三方登录
├── sign-up-form.tsx                     # 注册
├── forgot-password-form.tsx             # 找回密码(成功态用波纹)
├── password-strength.tsx                # 密码强度条与评分
├── social-buttons.tsx                   # GitHub / Google 按钮
├── form-parts.tsx                       # 输入框、密码框、错误提示等共用件
├── schemas.ts                           # zod 校验规则工厂与规则常量,提示语取自 labels
└── use-auth-form.ts                     # 受控表单状态 + 校验 + 提交状态
  • 校验规则与校验提示分开:规则常量(PASSWORD_MIN_LENGTH、NAME_MIN_LENGTH、CODE_LENGTH、PHONE_PATTERN / PHONE_LENGTH)在 schemas.ts,提示文案是 labels 的 validation* 键;提示里的 {min}、{length} 由常量填入,改规则后文案里的数字自动更新。schema 由 createSignInSchema(labels) 这类工厂函数生成,组件里用 useMemo 缓存,labels 引用不变就不会重建。
  • 前端校验不是安全边界:手机号、密码策略只是体验层,服务端必须再校验一次。
  • 静态导出:组件是纯客户端组件,不使用 Server Actions 或 cookies(),可用于 output: "export"。

如何自定义

  • 校验规则:规则常量都在 schemas.ts。手机号默认是中国大陆 11 位规则,面向其他地区时替换 PHONE_PATTERN 并同步 PHONE_LENGTH;密码策略是 PASSWORD_MIN_LENGTH 与两条正则(必须含字母、必须含数字);姓名最短长度是 NAME_MIN_LENGTH,验证码位数是 CODE_LENGTH。提示文案不用改,数字会跟着常量走;newPasswordPlaceholder(「至少 8 位,包含字母和数字」)是静态示例文案,改了最小长度记得一并改。
  • 强度条算法:password-strength.tsx 的 getPasswordStrength,按长度(8、12)与大小写、数字、符号计分,颜色从 destructive 过渡到 primary。
  • 只要登录:传 allowSignUp={false},再传 socialProviders={[]} 隐藏第三方登录(省略时默认会显示 GitHub 和 Google)。
  • 增减第三方平台:在 social-buttons.tsx 的 providers 里补一项,并扩展 SocialProvider 类型。
  • 改文案:优先用 labels,校验提示也一样,见上方换成英文;整站换语言时也可以直接改 config.ts 的 DEFAULT_LABELS。
  • 表单状态:刻意只用受控状态 + zod,没有引入 react-hook-form。字段首次失焦时校验该字段,提交失败后改为输入时实时校验。