本页由 3 个真实组件拼成;提示词与源码都可直接复制带走。
复现「两步验证」整页场景(/showcase/demo/auth):
- 页面级规则按 /design#pages 的对应小节执行,不在此复述
- 本页由这些真实组件拼成(手写等价物不算):
- 玻璃提示条(Notification Banner):页面级状态:常驻、不自动消失,处理完或手动关闭才走|/showcase/component/banner/glass-apple
- 玻璃按钮(Liquid Glass Button):半透明底 + 1px 细边框 + 背景模糊,做出玻璃厚度|/showcase/component/button/glass-apple
- 玻璃验证码输入(One-Time Code Input):一个真输入框 + 画的格子:逐格 input 会打断 autocomplete=one-time-code 自动填充|/showcase/component/otp/glass-apple
- 骨架:视口高度固定,页头不滚,正文在容器内滚;分区页的页头与正文共用同一条内容列
清单是扫 src/pages/showcase/demo/auth.astro 的 import 闭包得出的,
手写等价物不会出现在这里。
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import DemoChrome from '../../../components/DemoChrome.astro';
import PageHeader from '../../../components/PageHeader.astro';
import PageShell from '../../../components/PageShell.astro';
import SceneDocLink from '../../../components/SceneDocLink.astro';
import AuthVerify from './AuthVerify.tsx';
---
<BaseLayout title="UI Echo · 两步验证复现">
<DemoChrome />
<PageShell demo col colWidth="30rem">
<PageHeader
slot="header"
title="两步验证"
description="登录的第二步:验证码错了要看得懂后果,也要填得进去。"
size="sm"
>
<SceneDocLink id="auth" slot="actions" />
</PageHeader>
<!-- 单任务页:整列在正文区垂直居中,主操作留在表单末尾而不是页头右上 -->
<div class="my-auto">
<AuthVerify client:visible />
</div>
</PageShell>
</BaseLayout>
import { useState, type FormEvent } from 'react';
import { GlassAppleBanner } from '~/components/banner/glass-apple/Banner';
import { GlassAppleButton } from '~/components/button/glass-apple/Button';
import { GlassAppleOtp } from '~/components/otp/glass-apple/Otp';
type Status = 'idle' | 'error' | 'done';
const ACCOUNT = 'gqy@example.com';
/**
* 两步验证:验证码错了怎么办——页面级 banner 常驻说清后果,
* 字段级 errorMessage 说清该填什么,两层各管一件事(决策 17)。
*/
export default function AuthVerify() {
const [code, setCode] = useState('');
const [status, setStatus] = useState<Status>('idle');
const [busy, setBusy] = useState(false);
const [sentAt, setSentAt] = useState(1);
const submit = (e: FormEvent) => {
e.preventDefault();
if (code.length < 6) {
setStatus('error');
return;
}
setBusy(true);
window.setTimeout(() => {
setBusy(false);
setStatus('done');
setCode('');
}, 600);
};
return (
<div className="my-auto w-full space-y-5">
<p className="text-sm text-[var(--color-text-muted)]">
我们把 6 位验证码发到了 <span className="text-[var(--color-text)]">{ACCOUNT}</span>。
</p>
{status === 'error' && (
<GlassAppleBanner
tone="warning"
title="验证码不正确或已过期"
description="验证码 5 分钟内有效。超过 5 次错误会暂时锁定登录。"
action={
<GlassAppleButton
size="sm"
onClick={() => {
setSentAt((n) => n + 1);
setCode('');
setStatus('idle');
}}
>
重新发送
</GlassAppleButton>
}
/>
)}
{status === 'done' && (
<GlassAppleBanner tone="success" title="验证通过" description="这次登录已确认。" />
)}
<form onSubmit={submit} noValidate>
<GlassAppleOtp
label="验证码"
hint={`6 位数字,5 分钟内有效(第 ${sentAt} 次发送)`}
error={status === 'error'}
errorMessage={status === 'error' ? '只填数字,不需要空格或短横线' : undefined}
length={6}
value={code}
onChange={(v) => {
setCode(v);
if (status === 'error') setStatus('idle');
}}
/>
<div className="mt-6 flex items-center justify-between gap-4 flex-wrap">
<a
href="/showcase/demo/page-settings"
className="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text)] underline underline-offset-4"
>
用备用验证方式
</a>
<GlassAppleButton type="submit" loading={busy}>
{busy ? '验证中' : '验证并继续'}
</GlassAppleButton>
</div>
</form>
</div>
);
}
页面级规则(页头、工具栏、表格、设置、详情、流程、状态)只写在 页面模式 一处,这里不复述——复述就会漂移。