Files
Genarrative/src/components/auth/BindPhoneScreen.tsx
T
kdletters 7ea463ed08 退役旧创作模板业务并保留数据壳
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义
移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖
恢复现役创作、项目、我的入口及桌面移动导航
收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界
补齐开发栈、网关、原生壳和文档退役约束
2026-07-18 22:02:22 +08:00

215 lines
7.5 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
import type {
AuthCaptchaChallenge,
AuthUser,
} from '../../services/authService';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PLATFORM_BRAND_ASSETS } from '../common/platformBrandAssets';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
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(204,117,76,0.18),rgba(240,203,169,0.16))] 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__lockup">
<img
src={PLATFORM_BRAND_ASSETS.taonierProductIp}
alt=""
aria-hidden="true"
draggable={false}
className="selection-hero-brand__image"
/>
<div className="selection-hero-brand__title">陶泥儿</div>
</div>
<div className="selection-hero-brand__subtitle">
AI 美术创作平台
</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>
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-8 text-sm text-[var(--platform-text-base)]"
>
当前登录身份:{user.displayName}
</PlatformSubpanel>
</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">
<PlatformFieldLabel variant="form" className="mb-0">
手机号
</PlatformFieldLabel>
<PlatformTextField
autoComplete="tel"
inputMode="numeric"
value={phone}
onChange={(event) => setPhone(event.target.value)}
placeholder="13800000000"
/>
</label>
<label className="grid gap-2">
<PlatformFieldLabel variant="form" className="mb-0">
验证码
</PlatformFieldLabel>
<div className="flex gap-3">
<PlatformTextField
className="min-w-0 flex-1"
inputMode="numeric"
value={code}
onChange={(event) => setCode(event.target.value)}
placeholder="输入验证码"
/>
<PlatformActionButton
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
tone="secondary"
size="lg"
className="shrink-0 text-sm"
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`
: '获取验证码'}
</PlatformActionButton>
</div>
</label>
{hint ? (
<PlatformStatusMessage tone="success" surface="profile">
{hint}
</PlatformStatusMessage>
) : null}
<CaptchaChallengeField
challenge={captchaChallenge}
answer={captchaAnswer}
onAnswerChange={setCaptchaAnswer}
/>
{error ? (
<PlatformStatusMessage tone="error" surface="profile">
{error}
</PlatformStatusMessage>
) : null}
<PlatformActionButton
type="submit"
disabled={binding || !phone.trim() || !code.trim()}
size="lg"
>
{binding ? '正在绑定...' : '绑定手机号并继续'}
</PlatformActionButton>
<PlatformActionButton
tone="ghost"
size="md"
className="h-11"
onClick={() => {
void onLogout();
}}
>
返回其他登录方式
</PlatformActionButton>
</form>
</div>
</div>
</div>
);
}