7ea463ed08
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义 移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖 恢复现役创作、项目、我的入口及桌面移动导航 收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界 补齐开发栈、网关、原生壳和文档退役约束
215 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|