This commit is contained in:
182
src/components/auth/BindPhoneScreen.tsx
Normal file
182
src/components/auth/BindPhoneScreen.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { AuthCaptchaChallenge, AuthUser } from '../../services/authService';
|
||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
|
||||
type BindPhoneScreenProps = {
|
||||
user: AuthUser;
|
||||
platformTheme: PlatformTheme;
|
||||
sendingCode: boolean;
|
||||
binding: boolean;
|
||||
error: string;
|
||||
captchaChallenge: AuthCaptchaChallenge | null;
|
||||
onSendCode: (
|
||||
phone: string,
|
||||
captcha?: {
|
||||
challengeId?: string;
|
||||
answer?: string;
|
||||
},
|
||||
) => Promise<{
|
||||
cooldownSeconds: number;
|
||||
expiresInSeconds: number;
|
||||
}>;
|
||||
onSubmit: (phone: string, code: string) => Promise<void>;
|
||||
onLogout: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function BindPhoneScreen({
|
||||
user,
|
||||
platformTheme,
|
||||
sendingCode,
|
||||
binding,
|
||||
error,
|
||||
captchaChallenge,
|
||||
onSendCode,
|
||||
onSubmit,
|
||||
onLogout,
|
||||
}: BindPhoneScreenProps) {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
||||
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
||||
const [hint, setHint] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (cooldownSeconds <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setCooldownSeconds((current) => Math.max(0, current - 1));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [cooldownSeconds]);
|
||||
|
||||
return (
|
||||
<div className={`platform-theme platform-theme--${platformTheme} min-h-screen bg-[var(--platform-body-fill)] px-4 py-6 text-[var(--platform-text-strong)] sm:py-8`}>
|
||||
<div className="mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-5xl items-center justify-center sm:min-h-[calc(100vh-4rem)]">
|
||||
<div className="platform-auth-card grid w-full max-w-4xl overflow-hidden rounded-[28px] md:grid-cols-[1.05fr_0.95fr]">
|
||||
<div className="border-b border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(255,79,139,0.18),rgba(255,155,120,0.14))] px-6 py-8 md:border-b-0 md:border-r md:px-10 md:py-12">
|
||||
<div className="selection-hero-brand selection-hero-brand--left">
|
||||
<div className="selection-hero-brand__title">叙世</div>
|
||||
<div className="selection-hero-brand__subtitle">视觉叙事 RPG</div>
|
||||
</div>
|
||||
<p className="mt-8 text-[11px] font-semibold tracking-[0.32em] text-[var(--platform-cool-text)]">
|
||||
账号激活
|
||||
</p>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-tight text-[var(--platform-text-strong)] md:text-4xl">
|
||||
绑定手机号
|
||||
</h1>
|
||||
<p className="mt-4 max-w-md text-sm leading-7 text-[var(--platform-text-base)]">
|
||||
微信身份已建立,还差最后一步。绑定手机号后,你的账号才会正式激活,并同步到后端存档体系。
|
||||
</p>
|
||||
<div className="platform-subpanel mt-8 rounded-2xl px-4 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
当前登录身份:{user.displayName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="flex flex-col justify-center gap-5 px-6 py-8 md:px-10 md:py-12"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void onSubmit(phone, code);
|
||||
}}
|
||||
>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>手机号</span>
|
||||
<input
|
||||
className="platform-input"
|
||||
autoComplete="tel"
|
||||
inputMode="numeric"
|
||||
value={phone}
|
||||
onChange={(event) => setPhone(event.target.value)}
|
||||
placeholder="13800000000"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
<span>验证码</span>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="platform-input min-w-0 flex-1"
|
||||
inputMode="numeric"
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
placeholder="输入验证码"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
|
||||
className="platform-button platform-button--secondary h-12 shrink-0 px-4 text-sm disabled:cursor-not-allowed disabled:opacity-55"
|
||||
onClick={() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await onSendCode(phone, {
|
||||
challengeId: captchaChallenge?.challengeId,
|
||||
answer: captchaAnswer,
|
||||
});
|
||||
setCooldownSeconds(result.cooldownSeconds);
|
||||
setHint(
|
||||
`验证码已发送,有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
|
||||
);
|
||||
setCaptchaAnswer('');
|
||||
} catch {
|
||||
setHint('');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{sendingCode
|
||||
? '发送中...'
|
||||
: cooldownSeconds > 0
|
||||
? `${cooldownSeconds}s`
|
||||
: '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{hint ? (
|
||||
<div className="platform-banner platform-banner--success text-sm">
|
||||
{hint}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CaptchaChallengeField
|
||||
challenge={captchaChallenge}
|
||||
answer={captchaAnswer}
|
||||
onAnswerChange={setCaptchaAnswer}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger text-sm">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={binding || !phone.trim() || !code.trim()}
|
||||
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{binding ? '正在绑定...' : '绑定手机号并进入游戏'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="platform-button platform-button--ghost h-11 px-4 text-sm"
|
||||
onClick={() => {
|
||||
void onLogout();
|
||||
}}
|
||||
>
|
||||
返回其他登录方式
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user