[优化] 格式化

This commit is contained in:
ryan
2026-05-30 13:15:20 +08:00
parent 28b1305906
commit 9ec3d5b42d
110 changed files with 29155 additions and 7642 deletions
@@ -1,63 +1,73 @@
import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/utils/cn';
export function AuthFormField({
label,
hint,
children,
}: {
label: string;
hint?: string;
children: ReactNode;
}) {
return (
<label className='block space-y-2'>
<span className='text-sm font-medium text-[var(--foreground-primary)]'>{label}</span>
{children}
{hint ? <span className='block text-xs text-[var(--foreground-secondary)]'>{hint}</span> : null}
</label>
);
}
export function AuthInput(props: InputHTMLAttributes<HTMLInputElement>) {
return (
<input
{...props}
className={cn(
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)]',
props.className,
)}
/>
);
}
export function AuthButton({
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={cn(
'inline-flex w-full items-center justify-center rounded-2xl bg-[var(--brand-primary)] px-4 py-3 text-sm font-medium text-[var(--foreground-inverse)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
/>
);
}
export function SecondaryButton({
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={cn(
'inline-flex items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-4 py-3 text-sm font-medium text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)] disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
/>
);
}
import type {
ButtonHTMLAttributes,
InputHTMLAttributes,
ReactNode,
} from 'react';
import { cn } from '@/lib/utils/cn';
export function AuthFormField({
label,
hint,
children,
}: {
label: string;
hint?: string;
children: ReactNode;
}) {
return (
<label className="block space-y-2">
<span className="text-sm font-medium text-[var(--foreground-primary)]">
{label}
</span>
{children}
{hint ? (
<span className="block text-xs text-[var(--foreground-secondary)]">
{hint}
</span>
) : null}
</label>
);
}
export function AuthInput(props: InputHTMLAttributes<HTMLInputElement>) {
return (
<input
{...props}
className={cn(
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)]',
props.className,
)}
/>
);
}
export function AuthButton({
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={cn(
'inline-flex w-full items-center justify-center rounded-2xl bg-[var(--brand-primary)] px-4 py-3 text-sm font-medium text-[var(--foreground-inverse)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
/>
);
}
export function SecondaryButton({
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
className={cn(
'inline-flex items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-4 py-3 text-sm font-medium text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)] disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
/>
);
}
@@ -1,35 +1,38 @@
'use client';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
interface DashboardAuthGuardProps {
children: ReactNode;
}
export function DashboardAuthGuard({ children }: DashboardAuthGuardProps) {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
const redirect = pathname && pathname !== '/' ? `?redirect=${encodeURIComponent(pathname)}` : '';
router.replace(`/login${redirect}`);
}
}, [isAuthenticated, isLoading, pathname, router]);
if (isLoading || !isAuthenticated) {
return (
<main className='flex min-h-screen items-center justify-center px-4 py-8'>
<LoadingState className='w-full max-w-md' />
</main>
);
}
return <>{children}</>;
}
'use client';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
interface DashboardAuthGuardProps {
children: ReactNode;
}
export function DashboardAuthGuard({ children }: DashboardAuthGuardProps) {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
const redirect =
pathname && pathname !== '/'
? `?redirect=${encodeURIComponent(pathname)}`
: '';
router.replace(`/login${redirect}`);
}
}, [isAuthenticated, isLoading, pathname, router]);
if (isLoading || !isAuthenticated) {
return (
<main className="flex min-h-screen items-center justify-center px-4 py-8">
<LoadingState className="w-full max-w-md" />
</main>
);
}
return <>{children}</>;
}
@@ -1,44 +1,53 @@
'use client';
import { useMutation } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation';
'use client';
import { useMutation } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { InlineMessage } from '@/components/feedback/inline-message';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
import { AppCard } from '@/components/ui/app-card';
import { exchangeGitHubCode } from '@/features/auth/api/auth';
export function GitHubOAuthCallback() {
const router = useRouter();
const searchParams = useSearchParams();
import { InlineMessage } from '@/components/feedback/inline-message';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
import { AppCard } from '@/components/ui/app-card';
import { exchangeGitHubCode } from '@/features/auth/api/auth';
export function GitHubOAuthCallback() {
const router = useRouter();
const searchParams = useSearchParams();
const { setUser } = useAuth();
const handledCodeRef = useRef<string | null>(null);
const [prompt, setPrompt] = useState('正在处理 GitHub 授权结果...');
const [message, setMessage] = useState<{ tone: 'danger' | 'success'; text: string } | null>(null);
const [prompt, setPrompt] = useState('正在处理 GitHub 授权结果...');
const [message, setMessage] = useState<{
tone: 'danger' | 'success';
text: string;
} | null>(null);
const code = searchParams?.get('code')?.trim() ?? '';
const mutation = useMutation({
mutationFn: exchangeGitHubCode,
onSuccess: (user) => {
setUser(user);
setMessage({ tone: 'success', text: '登录成功,正在跳转...' });
router.replace('/');
},
onError: (error: Error) => {
setPrompt('授权处理失败');
setMessage({ tone: 'danger', text: error.message || 'GitHub 授权失败,请稍后重试。' });
},
});
useEffect(() => {
const mutation = useMutation({
mutationFn: exchangeGitHubCode,
onSuccess: (user) => {
setUser(user);
setMessage({ tone: 'success', text: '登录成功,正在跳转...' });
router.replace('/');
},
onError: (error: Error) => {
setPrompt('授权处理失败');
setMessage({
tone: 'danger',
text: error.message || 'GitHub 授权失败,请稍后重试。',
});
},
});
useEffect(() => {
if (!code) {
setPrompt('缺少授权 code');
setMessage({ tone: 'danger', text: '未收到 GitHub 授权参数,请返回登录页重试。' });
return;
}
setPrompt('缺少授权 code');
setMessage({
tone: 'danger',
text: '未收到 GitHub 授权参数,请返回登录页重试。',
});
return;
}
if (handledCodeRef.current === code) {
return;
}
@@ -46,13 +55,15 @@ export function GitHubOAuthCallback() {
handledCodeRef.current = code;
mutation.mutate(code);
}, [code, mutation]);
return (
<AppCard title='GitHub OAuth 回调' description={prompt}>
<div className='space-y-4'>
{mutation.isPending ? <LoadingState /> : null}
{message ? <InlineMessage tone={message.tone} message={message.text} /> : null}
</div>
</AppCard>
);
}
return (
<AppCard title="GitHub OAuth 回调" description={prompt}>
<div className="space-y-4">
{mutation.isPending ? <LoadingState /> : null}
{message ? (
<InlineMessage tone={message.tone} message={message.text} />
) : null}
</div>
</AppCard>
);
}
@@ -1,82 +1,106 @@
'use client';
import { useMutation } from '@tanstack/react-query';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { InlineMessage } from '@/components/feedback/inline-message';
import { AppCard } from '@/components/ui/app-card';
import { resetPassword } from '@/features/auth/api/auth';
import {
AuthButton,
AuthFormField,
AuthInput,
SecondaryButton,
} from '@/features/auth/components/auth-form-primitives';
export function PasswordResetConfirmForm() {
const searchParams = useSearchParams();
const email = searchParams?.get('email') || '';
const token = searchParams?.get('token') || '';
const [message, setMessage] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);
const mutation = useMutation({
mutationFn: () => resetPassword({ email, token }),
onSuccess: async (password) => {
try {
await navigator.clipboard.writeText(password);
setMessage({ tone: 'success', text: `密码已重置,新密码已复制到剪贴板:${password}` });
} catch {
setMessage({ tone: 'success', text: `密码已重置:${password}` });
}
},
onError: (error: Error) => {
setMessage({ tone: 'danger', text: error.message || '密码重置失败,请重新获取链接。' });
},
});
const missingParams = !email || !token;
return (
<AppCard title='密码重置确认' description='确认后,系统会生成新的随机密码。'>
<div className='space-y-4'>
<AuthFormField label='邮箱地址'>
<AuthInput value={email} readOnly />
</AuthFormField>
{missingParams ? (
<InlineMessage tone='danger' message='重置链接缺少必要参数,请重新发起密码重置。' />
) : null}
{message ? <InlineMessage tone={message.tone} message={message.text} /> : null}
<div className='flex flex-col gap-3 sm:flex-row'>
<AuthButton type='button' disabled={missingParams || mutation.isPending} onClick={() => mutation.mutate()}>
{mutation.isPending ? '处理中...' : '确认重置密码'}
</AuthButton>
{message?.tone === 'success' ? (
<SecondaryButton
type='button'
onClick={async () => {
const password = message.text.split(':').pop() || '';
if (password) {
await navigator.clipboard.writeText(password);
}
}}
>
再次复制密码
</SecondaryButton>
) : null}
</div>
<div className='text-sm text-[var(--foreground-secondary)]'>
处理完成后可返回
<Link href='/login' className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'>
登录页
</Link>
</div>
</div>
</AppCard>
);
}
'use client';
import { useMutation } from '@tanstack/react-query';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { InlineMessage } from '@/components/feedback/inline-message';
import { AppCard } from '@/components/ui/app-card';
import { resetPassword } from '@/features/auth/api/auth';
import {
AuthButton,
AuthFormField,
AuthInput,
SecondaryButton,
} from '@/features/auth/components/auth-form-primitives';
export function PasswordResetConfirmForm() {
const searchParams = useSearchParams();
const email = searchParams?.get('email') || '';
const token = searchParams?.get('token') || '';
const [message, setMessage] = useState<{
tone: 'success' | 'danger';
text: string;
} | null>(null);
const mutation = useMutation({
mutationFn: () => resetPassword({ email, token }),
onSuccess: async (password) => {
try {
await navigator.clipboard.writeText(password);
setMessage({
tone: 'success',
text: `密码已重置,新密码已复制到剪贴板:${password}`,
});
} catch {
setMessage({ tone: 'success', text: `密码已重置:${password}` });
}
},
onError: (error: Error) => {
setMessage({
tone: 'danger',
text: error.message || '密码重置失败,请重新获取链接。',
});
},
});
const missingParams = !email || !token;
return (
<AppCard
title="密码重置确认"
description="确认后,系统会生成新的随机密码。"
>
<div className="space-y-4">
<AuthFormField label="邮箱地址">
<AuthInput value={email} readOnly />
</AuthFormField>
{missingParams ? (
<InlineMessage
tone="danger"
message="重置链接缺少必要参数,请重新发起密码重置。"
/>
) : null}
{message ? (
<InlineMessage tone={message.tone} message={message.text} />
) : null}
<div className="flex flex-col gap-3 sm:flex-row">
<AuthButton
type="button"
disabled={missingParams || mutation.isPending}
onClick={() => mutation.mutate()}
>
{mutation.isPending ? '处理中...' : '确认重置密码'}
</AuthButton>
{message?.tone === 'success' ? (
<SecondaryButton
type="button"
onClick={async () => {
const password = message.text.split(':').pop() || '';
if (password) {
await navigator.clipboard.writeText(password);
}
}}
>
再次复制密码
</SecondaryButton>
) : null}
</div>
<div className="text-sm text-[var(--foreground-secondary)]">
处理完成后可返回
<Link
href="/login"
className="ml-2 text-[var(--brand-primary)] transition hover:opacity-80"
>
登录页
</Link>
</div>
</div>
</AppCard>
);
}
@@ -1,13 +1,19 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { PasswordResetConfirmForm } from '@/features/auth/components/password-reset-confirm-form';
import { PasswordResetRequestForm } from '@/features/auth/components/password-reset-request-form';
export function PasswordResetFlow() {
const searchParams = useSearchParams();
const hasConfirmationParams = Boolean(searchParams?.get('email') && searchParams?.get('token'));
return hasConfirmationParams ? <PasswordResetConfirmForm /> : <PasswordResetRequestForm />;
}
'use client';
import { useSearchParams } from 'next/navigation';
import { PasswordResetConfirmForm } from '@/features/auth/components/password-reset-confirm-form';
import { PasswordResetRequestForm } from '@/features/auth/components/password-reset-request-form';
export function PasswordResetFlow() {
const searchParams = useSearchParams();
const hasConfirmationParams = Boolean(
searchParams?.get('email') && searchParams?.get('token'),
);
return hasConfirmationParams ? (
<PasswordResetConfirmForm />
) : (
<PasswordResetRequestForm />
);
}
@@ -1,101 +1,120 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { InlineMessage } from '@/components/feedback/inline-message';
import { TurnstileWidget } from '@/components/forms/turnstile-widget';
import { AppCard } from '@/components/ui/app-card';
import { sendPasswordResetEmail } from '@/features/auth/api/auth';
import { getPublicStatus } from '@/features/auth/api/public';
import {
AuthButton,
AuthFormField,
AuthInput,
} from '@/features/auth/components/auth-form-primitives';
import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard';
const resetRequestSchema = z.object({
email: z.string().email('请输入有效邮箱地址'),
});
type ResetRequestFormValues = z.infer<typeof resetRequestSchema>;
export function PasswordResetRequestForm() {
const [turnstileToken, setTurnstileToken] = useState('');
const [message, setMessage] = useState<{ tone: 'success' | 'danger' | 'info'; text: string } | null>(null);
const form = useForm<ResetRequestFormValues>({
resolver: zodResolver(resetRequestSchema),
defaultValues: { email: '' },
});
const statusQuery = useQuery({
queryKey: ['public-status'],
queryFn: getPublicStatus,
});
const mutation = useMutation({
mutationFn: (values: ResetRequestFormValues) =>
sendPasswordResetEmail(values.email, turnstileToken || undefined),
onSuccess: () => {
setMessage({ tone: 'success', text: '重置邮件发送成功,请检查邮箱。' });
form.reset();
},
onError: (error: Error) => {
setMessage({ tone: 'danger', text: error.message || '重置邮件发送失败,请稍后重试。' });
},
});
const handleSubmit = form.handleSubmit((values) => {
setMessage(null);
if (statusQuery.data?.turnstile_check && !turnstileToken) {
setMessage({ tone: 'info', text: '请先完成人机验证。' });
return;
}
mutation.mutate(values);
});
return (
<PublicAuthGuard>
<AppCard title='密码重置' description='提交后,系统会向你的注册邮箱发送重置链接。'>
<form className='space-y-4' onSubmit={handleSubmit}>
<AuthFormField label='邮箱地址'>
<AuthInput type='email' placeholder='请输入邮箱地址' {...form.register('email')} />
{form.formState.errors.email ? (
<span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.email.message}
</span>
) : null}
</AuthFormField>
{statusQuery.data?.turnstile_check && statusQuery.data.turnstile_site_key ? (
<TurnstileWidget
siteKey={statusQuery.data.turnstile_site_key}
onVerify={(token) => setTurnstileToken(token)}
onExpire={() => setTurnstileToken('')}
onError={() => setTurnstileToken('')}
/>
) : null}
{message ? <InlineMessage tone={message.tone} message={message.text} /> : null}
<AuthButton type='submit' disabled={mutation.isPending}>
{mutation.isPending ? '提交中...' : '发送重置邮件'}
</AuthButton>
</form>
<div className='mt-6 text-sm text-[var(--foreground-secondary)]'>
想起密码了?
<Link href='/login' className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'>
返回登录
</Link>
</div>
</AppCard>
</PublicAuthGuard>
);
}
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { InlineMessage } from '@/components/feedback/inline-message';
import { TurnstileWidget } from '@/components/forms/turnstile-widget';
import { AppCard } from '@/components/ui/app-card';
import { sendPasswordResetEmail } from '@/features/auth/api/auth';
import { getPublicStatus } from '@/features/auth/api/public';
import {
AuthButton,
AuthFormField,
AuthInput,
} from '@/features/auth/components/auth-form-primitives';
import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard';
const resetRequestSchema = z.object({
email: z.string().email('请输入有效邮箱地址'),
});
type ResetRequestFormValues = z.infer<typeof resetRequestSchema>;
export function PasswordResetRequestForm() {
const [turnstileToken, setTurnstileToken] = useState('');
const [message, setMessage] = useState<{
tone: 'success' | 'danger' | 'info';
text: string;
} | null>(null);
const form = useForm<ResetRequestFormValues>({
resolver: zodResolver(resetRequestSchema),
defaultValues: { email: '' },
});
const statusQuery = useQuery({
queryKey: ['public-status'],
queryFn: getPublicStatus,
});
const mutation = useMutation({
mutationFn: (values: ResetRequestFormValues) =>
sendPasswordResetEmail(values.email, turnstileToken || undefined),
onSuccess: () => {
setMessage({ tone: 'success', text: '重置邮件发送成功,请检查邮箱。' });
form.reset();
},
onError: (error: Error) => {
setMessage({
tone: 'danger',
text: error.message || '重置邮件发送失败,请稍后重试。',
});
},
});
const handleSubmit = form.handleSubmit((values) => {
setMessage(null);
if (statusQuery.data?.turnstile_check && !turnstileToken) {
setMessage({ tone: 'info', text: '请先完成人机验证。' });
return;
}
mutation.mutate(values);
});
return (
<PublicAuthGuard>
<AppCard
title="密码重置"
description="提交后,系统会向你的注册邮箱发送重置链接。"
>
<form className="space-y-4" onSubmit={handleSubmit}>
<AuthFormField label="邮箱地址">
<AuthInput
type="email"
placeholder="请输入邮箱地址"
{...form.register('email')}
/>
{form.formState.errors.email ? (
<span className="text-xs text-[var(--status-danger-foreground)]">
{form.formState.errors.email.message}
</span>
) : null}
</AuthFormField>
{statusQuery.data?.turnstile_check &&
statusQuery.data.turnstile_site_key ? (
<TurnstileWidget
siteKey={statusQuery.data.turnstile_site_key}
onVerify={(token) => setTurnstileToken(token)}
onExpire={() => setTurnstileToken('')}
onError={() => setTurnstileToken('')}
/>
) : null}
{message ? (
<InlineMessage tone={message.tone} message={message.text} />
) : null}
<AuthButton type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '提交中...' : '发送重置邮件'}
</AuthButton>
</form>
<div className="mt-6 text-sm text-[var(--foreground-secondary)]">
想起密码了?
<Link
href="/login"
className="ml-2 text-[var(--brand-primary)] transition hover:opacity-80"
>
返回登录
</Link>
</div>
</AppCard>
</PublicAuthGuard>
);
}
@@ -1,35 +1,35 @@
'use client';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
interface PublicAuthGuardProps {
children: ReactNode;
}
export function PublicAuthGuard({ children }: PublicAuthGuardProps) {
const router = useRouter();
const searchParams = useSearchParams();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && isAuthenticated) {
const redirect = searchParams?.get('redirect');
router.replace(redirect || '/');
}
}, [isAuthenticated, isLoading, router, searchParams]);
if (isLoading) {
return <LoadingState />;
}
if (isAuthenticated) {
return null;
}
return <>{children}</>;
}
'use client';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { LoadingState } from '@/components/feedback/loading-state';
import { useAuth } from '@/components/providers/auth-provider';
interface PublicAuthGuardProps {
children: ReactNode;
}
export function PublicAuthGuard({ children }: PublicAuthGuardProps) {
const router = useRouter();
const searchParams = useSearchParams();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && isAuthenticated) {
const redirect = searchParams?.get('redirect');
router.replace(redirect || '/');
}
}, [isAuthenticated, isLoading, router, searchParams]);
if (isLoading) {
return <LoadingState />;
}
if (isAuthenticated) {
return null;
}
return <>{children}</>;
}
@@ -1,23 +1,23 @@
import { FeaturePlaceholder } from '@/components/feedback/feature-placeholder';
interface PublicRoutePlaceholderProps {
title: string;
description: string;
}
export function PublicRoutePlaceholder({
title,
description,
}: PublicRoutePlaceholderProps) {
return (
<FeaturePlaceholder
title={title}
description={description}
milestones={[
'认证页路由入口已建立,便于阶段 2 接入真实表单。',
'公共布局已统一,可复用品牌区、说明区与回跳入口。',
'后续将在此处补齐表单校验、Session 兼容与 OAuth 回调处理。',
]}
/>
);
}
import { FeaturePlaceholder } from '@/components/feedback/feature-placeholder';
interface PublicRoutePlaceholderProps {
title: string;
description: string;
}
export function PublicRoutePlaceholder({
title,
description,
}: PublicRoutePlaceholderProps) {
return (
<FeaturePlaceholder
title={title}
description={description}
milestones={[
'认证页路由入口已建立,便于阶段 2 接入真实表单。',
'公共布局已统一,可复用品牌区、说明区与回跳入口。',
'后续将在此处补齐表单校验、Session 兼容与 OAuth 回调处理。',
]}
/>
);
}