Files
Genarrative/src/components/auth/BindPhoneScreen.tsx
T
kdletters c5200c7d45
Project CI / Backend tests (push) Successful in 8m31s
Project CI / Repository checks (push) Successful in 14m14s
Project CI / Frontend tests (push) Successful in 13m2s
Project CI / Native shell tests (push) Successful in 21m30s
补齐共享 UI 组件库与展示页交互 (#202)
## 变更内容

- 新增基于 shadcn open-code 模式的共享 UI canonical 组件、样式与导出。
- 新增 `/components` 共享组件展示页,并覆盖平台组件、Token、状态和移动端布局。
- 补齐平台展示页的筛选、排序、上传预览、标签、开关和异步状态交互。
- 修复排序导致预览主题变化、筛选弹窗误关闭和入口状态不更新的问题。
- 同步网站与客户端构建别名、依赖、路由测试和项目文档。

## 验证

- `npm run test -- src/components/shared-components/SharedComponentsShowcasePage.test.tsx`
- `npm run typecheck`
- `npm run check:encoding`
- `npm run build:raw`
- `git diff --check`
- pre-push 门禁通过

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/202
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-08-31 14:10:44 +08:00

217 lines
7.3 KiB
TypeScript

import {
PlatformActionButton,
PlatformFieldLabel,
PlatformStatusMessage,
PlatformSubpanel,
PlatformTextField,
} from '@genarrative/shared/components';
import { useEffect, useState } from 'react';
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
import type {
AuthCaptchaChallenge,
AuthUser,
} from '../../services/authService';
import { PLATFORM_BRAND_ASSETS } from '../common/platformBrandAssets';
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>
);
}