1
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { type ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
@@ -7,6 +7,13 @@ import type {
|
||||
AuthLoginMethod,
|
||||
} from '../../services/authService';
|
||||
import { getStoredLastLoginPhone } from '../../services/authService';
|
||||
import { LegalDocumentModal } from '../common/LegalDocumentModal';
|
||||
import {
|
||||
getLegalDocument,
|
||||
type LegalDocumentId,
|
||||
persistLegalConsent,
|
||||
readStoredLegalConsent,
|
||||
} from '../common/legalDocuments';
|
||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
|
||||
type SmsScene = 'login' | 'reset_password';
|
||||
@@ -70,6 +77,9 @@ export function LoginScreen({
|
||||
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
||||
const [resetCooldownSeconds, setResetCooldownSeconds] = useState(0);
|
||||
const [hint, setHint] = useState('');
|
||||
const [legalConsentChecked, setLegalConsentChecked] = useState(false);
|
||||
const [activeLegalDocumentId, setActiveLegalDocumentId] =
|
||||
useState<LegalDocumentId | null>(null);
|
||||
const passwordLoginEnabled = availableLoginMethods.includes('password');
|
||||
const phoneLoginEnabled = availableLoginMethods.includes('phone');
|
||||
const wechatLoginEnabled = availableLoginMethods.includes('wechat');
|
||||
@@ -92,6 +102,8 @@ export function LoginScreen({
|
||||
setCooldownSeconds(0);
|
||||
setResetCooldownSeconds(0);
|
||||
setHint('');
|
||||
setLegalConsentChecked(readStoredLegalConsent());
|
||||
setActiveLegalDocumentId(null);
|
||||
setActiveLoginTab(phoneLoginEnabled ? 'phone' : 'password');
|
||||
}, [isOpen, phoneLoginEnabled]);
|
||||
|
||||
@@ -143,89 +155,117 @@ export function LoginScreen({
|
||||
}
|
||||
|
||||
const submitDisabled = loggingIn || sendingCode;
|
||||
const activeLegalDocument = activeLegalDocumentId
|
||||
? getLegalDocument(activeLegalDocumentId)
|
||||
: null;
|
||||
const toggleLegalConsent = () => {
|
||||
setLegalConsentChecked((current) => {
|
||||
const nextChecked = !current;
|
||||
if (nextChecked) {
|
||||
persistLegalConsent();
|
||||
}
|
||||
return nextChecked;
|
||||
});
|
||||
};
|
||||
const legalConsentRow = (
|
||||
<LegalConsentRow
|
||||
checked={legalConsentChecked}
|
||||
onToggle={toggleLegalConsent}
|
||||
onOpenDocument={setActiveLegalDocumentId}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[120] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="auth-login-dialog-title"
|
||||
className="platform-auth-card w-full max-w-md overflow-hidden rounded-[2rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[120] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div
|
||||
id="auth-login-dialog-title"
|
||||
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{isResetPanelOpen ? '重置密码' : '账号入口'}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="auth-login-dialog-title"
|
||||
className="platform-auth-card w-full max-w-md overflow-hidden rounded-[2rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div
|
||||
id="auth-login-dialog-title"
|
||||
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{isResetPanelOpen ? '重置密码' : '账号入口'}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-icon-button p-2"
|
||||
aria-label="关闭登录弹窗"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-icon-button p-2"
|
||||
aria-label="关闭登录弹窗"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isResetPanelOpen ? (
|
||||
<PasswordResetPanel
|
||||
phone={resetPhone}
|
||||
code={resetCode}
|
||||
password={resetPasswordValue}
|
||||
sendingCode={sendingCode}
|
||||
loggingIn={loggingIn}
|
||||
cooldownSeconds={resetCooldownSeconds}
|
||||
error={error}
|
||||
onPhoneChange={setResetPhone}
|
||||
onCodeChange={setResetCode}
|
||||
onPasswordChange={setResetPasswordValue}
|
||||
onBack={() => setIsResetPanelOpen(false)}
|
||||
onSendCode={async () => {
|
||||
const result = await onSendCode(resetPhone, 'reset_password');
|
||||
setResetCooldownSeconds(result.cooldownSeconds);
|
||||
}}
|
||||
onSubmit={() =>
|
||||
onResetPassword(resetPhone, resetCode, resetPasswordValue)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 px-5 py-5">
|
||||
{phoneLoginEnabled ? (
|
||||
<div
|
||||
className={`grid gap-2 ${
|
||||
passwordLoginEnabled ? 'grid-cols-2' : 'grid-cols-1'
|
||||
}`}
|
||||
role="tablist"
|
||||
aria-label="登录方式"
|
||||
>
|
||||
<LoginTabButton
|
||||
active={activeLoginTab === 'phone'}
|
||||
onClick={() => setActiveLoginTab('phone')}
|
||||
{isResetPanelOpen ? (
|
||||
<PasswordResetPanel
|
||||
phone={resetPhone}
|
||||
code={resetCode}
|
||||
password={resetPasswordValue}
|
||||
sendingCode={sendingCode}
|
||||
loggingIn={loggingIn}
|
||||
cooldownSeconds={resetCooldownSeconds}
|
||||
error={error}
|
||||
onPhoneChange={setResetPhone}
|
||||
onCodeChange={setResetCode}
|
||||
onPasswordChange={setResetPasswordValue}
|
||||
onBack={() => setIsResetPanelOpen(false)}
|
||||
onSendCode={async () => {
|
||||
const result = await onSendCode(resetPhone, 'reset_password');
|
||||
setResetCooldownSeconds(result.cooldownSeconds);
|
||||
}}
|
||||
onSubmit={() =>
|
||||
onResetPassword(resetPhone, resetCode, resetPasswordValue)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 px-5 py-5">
|
||||
{phoneLoginEnabled ? (
|
||||
<div
|
||||
className={`grid gap-2 ${
|
||||
passwordLoginEnabled ? 'grid-cols-2' : 'grid-cols-1'
|
||||
}`}
|
||||
role="tablist"
|
||||
aria-label="登录方式"
|
||||
>
|
||||
短信登录
|
||||
</LoginTabButton>
|
||||
{passwordLoginEnabled ? (
|
||||
<LoginTabButton
|
||||
active={activeLoginTab === 'password'}
|
||||
onClick={() => setActiveLoginTab('password')}
|
||||
active={activeLoginTab === 'phone'}
|
||||
onClick={() => setActiveLoginTab('phone')}
|
||||
>
|
||||
密码登录
|
||||
短信登录
|
||||
</LoginTabButton>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{passwordLoginEnabled ? (
|
||||
<LoginTabButton
|
||||
active={activeLoginTab === 'password'}
|
||||
onClick={() => setActiveLoginTab('password')}
|
||||
>
|
||||
密码登录
|
||||
</LoginTabButton>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{passwordLoginEnabled && activeLoginTab === 'password' ? (
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
submitDisabled ||
|
||||
!phone.trim() ||
|
||||
!password.trim() ||
|
||||
!legalConsentChecked
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void onPasswordSubmit(phone, password);
|
||||
}}
|
||||
>
|
||||
@@ -253,12 +293,16 @@ export function LoginScreen({
|
||||
</label>
|
||||
|
||||
{error ? <ErrorBanner message={error} /> : null}
|
||||
{legalConsentRow}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
submitDisabled || !phone.trim() || !password.trim()
|
||||
submitDisabled ||
|
||||
!phone.trim() ||
|
||||
!password.trim() ||
|
||||
!legalConsentChecked
|
||||
}
|
||||
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
@@ -296,6 +340,8 @@ export function LoginScreen({
|
||||
hint={hint}
|
||||
submitLabel="登录"
|
||||
enabled={phoneLoginEnabled}
|
||||
legalConsentChecked={legalConsentChecked}
|
||||
legalConsentNode={legalConsentRow}
|
||||
showPhoneField
|
||||
onPhoneChange={setPhone}
|
||||
onCodeChange={setCode}
|
||||
@@ -323,13 +369,89 @@ export function LoginScreen({
|
||||
当前登录入口暂不可用。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<LegalDocumentModal
|
||||
document={activeLegalDocument}
|
||||
open={Boolean(activeLegalDocument)}
|
||||
platformTheme={platformTheme}
|
||||
onClose={() => setActiveLegalDocumentId(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LegalConsentRow({
|
||||
checked,
|
||||
onToggle,
|
||||
onOpenDocument,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
onOpenDocument: (documentId: LegalDocumentId) => void;
|
||||
}) {
|
||||
const openDocument = (documentId: LegalDocumentId) => {
|
||||
onOpenDocument(documentId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2.5 text-xs leading-5 text-[var(--platform-text-base)]">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label="同意法律协议"
|
||||
onClick={onToggle}
|
||||
className={`mt-0.5 flex h-5 w-9 shrink-0 items-center rounded-full border p-0.5 transition ${
|
||||
checked
|
||||
? 'justify-end border-[var(--platform-button-primary-border)] [background:var(--platform-profile-action-fill)]'
|
||||
: 'justify-start border-[var(--platform-subpanel-border)] [background:var(--platform-button-secondary-fill)]'
|
||||
}`}
|
||||
>
|
||||
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--platform-button-primary-text)] text-[var(--platform-cool-text)] shadow-sm">
|
||||
{checked ? <Check className="h-3 w-3" /> : null}
|
||||
</span>
|
||||
</button>
|
||||
<div>
|
||||
我已阅读并同意
|
||||
<LegalLink
|
||||
label="《用户协议》"
|
||||
onClick={() => openDocument('user-agreement')}
|
||||
/>
|
||||
<LegalLink
|
||||
label="《隐私政策》"
|
||||
onClick={() => openDocument('privacy-policy')}
|
||||
/>
|
||||
和
|
||||
<LegalLink
|
||||
label="《免责声明》"
|
||||
onClick={() => openDocument('disclaimer')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LegalLink({
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-0.5 align-baseline font-semibold text-[var(--platform-cool-text)] underline-offset-2 hover:underline"
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginTabButton({
|
||||
active,
|
||||
children,
|
||||
@@ -371,6 +493,8 @@ function PhoneCodeForm({
|
||||
hint,
|
||||
submitLabel,
|
||||
enabled,
|
||||
legalConsentChecked,
|
||||
legalConsentNode,
|
||||
showPhoneField,
|
||||
onPhoneChange,
|
||||
onCodeChange,
|
||||
@@ -389,6 +513,8 @@ function PhoneCodeForm({
|
||||
hint: string;
|
||||
submitLabel: string;
|
||||
enabled: boolean;
|
||||
legalConsentChecked: boolean;
|
||||
legalConsentNode: ReactNode;
|
||||
showPhoneField: boolean;
|
||||
onPhoneChange: (value: string) => void;
|
||||
onCodeChange: (value: string) => void;
|
||||
@@ -400,11 +526,17 @@ function PhoneCodeForm({
|
||||
return null;
|
||||
}
|
||||
|
||||
const submitBlocked =
|
||||
loggingIn || !phone.trim() || !code.trim() || !legalConsentChecked;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (submitBlocked) {
|
||||
return;
|
||||
}
|
||||
void onSubmit();
|
||||
}}
|
||||
>
|
||||
@@ -455,10 +587,11 @@ function PhoneCodeForm({
|
||||
|
||||
{hint ? <SuccessBanner message={hint} /> : null}
|
||||
{error ? <ErrorBanner message={error} /> : null}
|
||||
{legalConsentNode}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loggingIn || !phone.trim() || !code.trim()}
|
||||
disabled={submitBlocked}
|
||||
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{loggingIn ? '处理中' : submitLabel}
|
||||
|
||||
Reference in New Issue
Block a user