feat(web): full auth screens rework to match design + old-app business rules
Rebuilt tenant/login/register/reset-password to match the design handoff's 01-account screens precisely (previously only loosely approximated), and ported the old app's real business rules instead of guessing: - LoginPage: desktop split branding panel (>=900px), terms-agreement checkbox gating submit, forgot-password link, password visibility toggle, full ctrl-error translation table (lib/tinode/errors.ts, ported from the old app's CTRL_ERROR_TEXT_CN/translateCtrlError -- registration/login go over native WS, not REST, so client.ts's REASON_MESSAGES never applied). - RegisterPage: rebuilt as the real unified username-OR-phone form (not phone-only) -- username optional unless phone is blank, invite code required+validated only on the username-only path, matching the server's actual resolveRegistrationAccount rules. connection.ts's registerAccountByPhone generalized into registerAccount() supporting both paths, including the Tinode.credential(null) crash the old app had to work around on the username-only path. - New ResetPasswordPage wired to /auth/password-reset/* (a different endpoint family than registration's /auth/verification-codes/*). - Shared components: PasswordInput, VerificationCodeButton, Agreement, Toast. - Fixed tinode-sdk.d.ts: the real method is getCurrentUserID(), not the invented getAuthenticatedUserID() -- caught live, login crashed past a successful auth. - Fixed a responsive bug present on all four auth pages: fixed w-[380px] forms overflowed past the viewport edge below ~430px wide. Verified end-to-end against the real backend (api.118im.com, tenant "default"): fresh phone registration, password reset, re-login with the new password, and the username-only path's invite-code rejection all round-tripped correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
63c1b84b41
commit
aa1c46ff22
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Icon } from '@/app/Icon';
|
||||
|
||||
interface PasswordInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
autoComplete?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function PasswordInput({ value, onChange, placeholder, autoComplete, error }: PasswordInputProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div
|
||||
className={`flex h-[50px] items-center rounded-lg bg-surface-input px-3.5 has-focus:border has-focus:shadow-focus-ring ${
|
||||
error ? 'border border-danger-border' : 'has-focus:border-focus-border'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type={visible ? 'text' : 'password'}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
className="w-full bg-transparent text-[15px] outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVisible((v) => !v)}
|
||||
aria-label={visible ? '隐藏密码' : '显示密码'}
|
||||
className="text-text-placeholder-1"
|
||||
>
|
||||
<Icon name={visible ? 'visibility' : 'visibility_off'} className="text-[20px]" />
|
||||
</button>
|
||||
</div>
|
||||
{error ? <div className="text-xs text-danger-border">{error}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user