安装
pnpm dlx shadcn@latest add https://yiui.dev/r/auth-07.json--dry-run 或 --diff 看清差异再装。app/lock/page.tsx,安装后就是一个可以访问的路由。生产项目里请按需删除,或改成自己的路径。用法
<Auth07
user={{ name: "林薇", email: "linwei@example.com", avatar: "/me.png" }}
reason="expired"
onUnlock={async ({ method, secret }) => {
await reauthenticate(method, secret)
}}
onSuccess={() => router.refresh()}
onSwitchAccount={() => router.push("/login")}
onSignOut={() => signOut()}
/>用户已经登录过,锁屏只需要再确认一次身份,所以不问账号,只要密码或 PIN。页面打开时自动聚焦当前验证方式的输入框;PIN 输满自动提交,密码按回车或点「解锁」提交。
onUnlock 返回 Promise<void>,抛错即验证失败:输入区轻轻左右晃一下,输入被清空,下方显示「抛出的原因 + 还可以尝试 N 次」,例如「密码错误,还可以尝试 2 次」。抛出的 Error 没有 message 时,原因用「密码错误」或「PIN 错误」。
失败次数与锁定
组件自己记失败次数。连续失败达到 maxAttempts(默认 5)次后锁定 lockSeconds(默认 30)秒,期间输入框和按钮都不可用,显示「尝试次数过多,请 N 秒后再试」的倒计时,到时自动恢复并聚焦输入框,失败次数清零。
服务端已经在限流时,在抛出的错误上带数字属性 retryAfter(秒),组件立即按它锁定:
const error = Object.assign(new Error("尝试过于频繁"), { retryAfter: 60 })
throw error这只是界面上的节流,真正的次数限制必须在服务端做。
时钟与时区
大号时钟只显示时和分,下面一行是日期和星期。传 timeZone(IANA 名称,如 Asia/Shanghai)时按它显示;不传时在浏览器挂载后读取本机时区。服务端渲染和水合期间时钟显示 --:--、日期行留空,不读取当前时间,不会出现水合不一致。问候语(「下午好,林薇」)按访客本地时间选取,挂载前显示「你好,林薇」。
示例
演示页的密码是 yiui2026,PIN 是 123456,其他输入都算错误。为了方便体验,演示页把 maxAttempts 设为 3、lockSeconds 设为 5:连续输错 3 次后锁定 5 秒。解锁后右上角出现「重新锁定」,可以反复体验。「切换账号」「退出登录」在演示页只显示一句说明。
API
当前登录的用户。
类型Auth07User验证密码或 PIN,失败时抛出 Error;错误对象带数字属性 retryAfter(秒)时立即锁定这么久。
(values: { method: Auth07Method; secret: string; }) => Promise<void>验证成功后的通知。
类型((values: { method: Auth07Method; }) => void) | undefined切换账号;未传时不显示入口。
类型(() => void) | undefined退出登录;未传时不显示入口。
类型(() => void) | undefined锁屏原因:手动锁定或会话过期。@default "locked"
类型Auth07Reason | undefined可用的验证方式,只有一种时不显示切换链接。@default ["password", "pin"]
类型Auth07Method[] | undefined受控的验证方式。
类型Auth07Method | undefined初始验证方式;默认取 methods 的第一项。
Auth07Method | undefined验证方式变化通知。
类型((method: Auth07Method) => void) | undefinedPIN 位数,范围 4–8。@default 6
类型number | undefined连续失败多少次后锁定。@default 5
类型number | undefined锁定秒数。@default 30
类型number | undefined时钟使用的 IANA 时区;未传时在挂载后取浏览器时区。
类型string | undefined时间与日期的区域格式。@default "zh-CN"
类型string | undefined覆盖默认中文文案。
类型Partial<{ region: string; clock: string; locked: string; expired: string; form: string; password: string; pin: string; showPassword: string; hidePassword: string; capsLock: string; unlock: string; unlocking: string; usePin: string; usePassword: string; switchAccount: string; signOut: string; wrongPassword: string; wrongPin: string; attempts: string; lockedOut: string; unlocked: string; unlockedDescription: string; }> | undefined- 验证方式:
methods默认["password", "pin"],第一项是默认方式;只给一种时不显示「改用 PIN / 改用密码」链接。受控用method搭配onMethodChange,非受控用defaultMethod(默认取methods的第一项)。受控值不在methods里时退回第一项。 reason:"locked"显示「屏幕已锁定」,"expired"显示「登录已过期,请重新验证身份」。user:name必填,用于问候语和头像占位(取第一个字);avatar是头像地址;email只写进隐藏的用户名字段,方便密码管理器识别账号。pinLength默认 6,范围 4–8;locale默认zh-CN,决定时间和日期格式。onSuccess({ method })在验证通过、界面切到「已解锁」后调用;onSwitchAccount、onSignOut没传时不显示对应链接。labels覆盖Auth07Labels,模板里的{message}、{count}、{seconds}、{length}、{name}会被替换。
导出类型:Auth07Props、Auth07Method、Auth07Reason、Auth07User、Auth07Labels。
文件结构:auth-07.tsx 为入口;form-parts.tsx 是时钟、密码框和 PIN 格子;use-auth-form.ts 管理提交、失败次数和锁定倒计时;config.ts 管理文案;types.ts 定义 API;page.tsx 为演示页面。
| data-slot | 说明 |
|---|---|
auth-07 | 根节点,带 data-method(password / pin)、data-state(idle / pending / error / locked / unlocked)和 data-reason |
auth-07-panel | 居中的内容带 |
auth-07-clock | 时钟与日期,挂载并取得时间后带 data-ready |
auth-07-reason | 锁屏原因 |
auth-07-form / auth-07-field | 验证表单与晃动的输入区 |
auth-07-pin / auth-07-pin-slot | PIN 输入与每一格(data-active、data-filled) |
auth-07-message | 错误、倒计时和大写锁定提示所在的一行 |
auth-07-error / auth-07-lockout / auth-07-caps-lock | 验证错误、锁定倒计时、大写锁定提示 |
auth-07-switch-method | 「改用 PIN / 改用密码」链接 |
auth-07-success | 已解锁状态 |
auth-07-footer | 切换账号、退出登录 |
可访问性
- 打开、切换验证方式、验证失败和锁定结束后,焦点都回到当前方式的输入框;解锁后焦点落在「已解锁」标题上,说明文字用
role="status"播报。 - 验证错误用
role="alert",并通过aria-describedby关联到输入框,输入框带aria-invalid;重新输入立即清除错误。 - 锁定倒计时不逐秒播报:读屏只在锁定开始时念一次「尝试次数过多,请 N 秒后再试」。
- 密码框可切换显示,切换按钮带
aria-pressed;按键时检测到大写锁定,在条下用role="status"提示。 - PIN 格子只显示圆点,读屏名称为「PIN 码,共 6 位」,手机上弹出数字键盘。
- 背景字符场是装饰,
aria-hidden,并在系统开启减少动态效果时静止;输错时的晃动在该设置下也会关闭。
注意事项
只提供界面,不负责会话、加密或真实的次数限制。组件内的失败计数在刷新页面后会清零,服务端必须自己限制尝试次数,并在需要时通过 retryAfter 告诉界面锁多久。根节点是 min-h-svh 的整屏布局,适合单独作为一个路由或覆盖在应用之上。