fix: clone source login experience

This commit is contained in:
2026-05-19 15:37:00 +08:00
parent 091a19556c
commit 2f2ea06767
4 changed files with 4172 additions and 308 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,28 @@ import type { FormEvent } from 'react';
import { Suspense, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { AnimatedLoginCharacters, type LoginCharacterMood } from '@/components/login/AnimatedLoginCharacters';
import { OasisCanvas } from '@/components/login/OasisCanvas';
type LoginStatus = 'idle' | 'loading' | 'success';
function Icon({ type }: { type: 'user' | 'lock' | 'eye' | 'eyeOff' | 'arrow' | 'check' }) {
const common = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
function Icon({
type,
className = 'h-4 w-4',
}: {
type: 'user' | 'lock' | 'eye' | 'eyeOff' | 'arrow' | 'check';
className?: string;
}) {
const common = {
className,
width: 16,
height: 16,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2.2,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
};
if (type === 'user') return <svg {...common}><path d="M20 21a8 8 0 0 0-16 0" /><circle cx="12" cy="7" r="4" /></svg>;
if (type === 'lock') return <svg {...common}><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></svg>;
if (type === 'eye') return <svg {...common}><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" /></svg>;
@@ -25,7 +42,6 @@ function LoginInner() {
const [showPassword, setShowPassword] = useState(false);
const [activeField, setActiveField] = useState<'username' | 'password' | null>(null);
const [hasError, setHasError] = useState(false);
const [message, setMessage] = useState('');
const [status, setStatus] = useState<LoginStatus>('idle');
const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 });
@@ -53,10 +69,8 @@ function LoginInner() {
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setHasError(false);
setMessage('');
if (!username.trim() || !password) {
setHasError(true);
setMessage('账号和密码都要填。');
return;
}
@@ -75,88 +89,105 @@ function LoginInner() {
} catch {
setStatus('idle');
setHasError(true);
setMessage('账号或密码不对。');
}
}
return (
<main className="login-page">
<div className="login-backdrop" />
<section className="login-shell">
<div className="login-showcase">
<div className="login-brand-mark">AI Toy</div>
<h1>AI Toy Patent</h1>
<p> IP </p>
<div className="login-character-strip">
<main className="login-page login-page--oasis login-page--source relative min-h-screen overflow-hidden text-white">
<OasisCanvas />
<div className="login-oasis-shade" />
<div className="login-source-overlay">
<section className="login-auth-panel login-source-auth-panel login-source-combo-panel rounded-[8px]">
<div className="login-top-brand" aria-hidden="true">
<span className="login-top-brand__logo login-top-brand__wordmark">AI Toy Patent</span>
<span className="login-top-brand__system"> IP </span>
</div>
<div className="login-source-character-strip" aria-hidden="true">
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
</div>
</div>
<form className="login-panel" onSubmit={onSubmit}>
<div>
<div className="login-panel__eyebrow">Private Workspace</div>
<h2></h2>
</div>
<form className="login-source-form-pane w-full" onSubmit={onSubmit}>
<div className="space-y-3">
<label className="block">
<span className="flex h-11 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#d6b36a] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#d6b36a]/25">
<Icon type="user" className="h-4 w-4 text-white/45" />
<input
className="h-full min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-white/30"
value={username}
disabled={disabled}
autoComplete="username"
aria-label="账号"
onFocus={() => setActiveField('username')}
onBlur={() => setActiveField(null)}
onChange={event => {
setUsername(event.target.value);
if (hasError) setHasError(false);
}}
/>
</span>
</label>
<label className="login-field">
<span><Icon type="user" /></span>
<input
value={username}
disabled={disabled}
autoComplete="username"
placeholder="账号"
onFocus={() => setActiveField('username')}
onBlur={() => setActiveField(null)}
onChange={event => {
setUsername(event.target.value);
if (hasError) setHasError(false);
}}
/>
</label>
<label className="block">
<span className="flex h-11 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#d6b36a] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#d6b36a]/25">
<Icon type="lock" className="h-4 w-4 text-white/45" />
<input
className="h-full min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-white/30"
value={password}
disabled={disabled}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
aria-label="密码"
onFocus={() => setActiveField('password')}
onBlur={() => setActiveField(null)}
onChange={event => {
setPassword(event.target.value);
if (hasError) setHasError(false);
}}
/>
<button
className="grid h-9 w-9 place-items-center rounded-[8px] text-white/55 transition hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-[#d6b36a]/45 disabled:opacity-50"
type="button"
disabled={disabled}
onMouseDown={event => event.preventDefault()}
onClick={() => setShowPassword(value => !value)}
aria-label={showPassword ? '隐藏密码' : '显示密码'}
>
<Icon type={showPassword ? 'eyeOff' : 'eye'} className="h-4 w-4" />
</button>
</span>
</label>
</div>
<label className="login-field">
<span><Icon type="lock" /></span>
<input
value={password}
disabled={disabled}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder="密码"
onFocus={() => setActiveField('password')}
onBlur={() => setActiveField(null)}
onChange={event => {
setPassword(event.target.value);
if (hasError) setHasError(false);
}}
/>
<button
type="button"
className="login-icon-button"
disabled={disabled}
onMouseDown={event => event.preventDefault()}
onClick={() => setShowPassword(value => !value)}
aria-label={showPassword ? '隐藏密码' : '显示密码'}
>
<Icon type={showPassword ? 'eyeOff' : 'eye'} />
</button>
</label>
<div className="login-options">
<label>
<input type="checkbox" checked={remember} disabled={disabled} onChange={event => setRemember(event.target.checked)} />
<label className="mt-3 flex cursor-pointer items-center gap-2 text-sm text-white/60">
<input
className="h-4 w-4 rounded border-white/20 bg-black/30 accent-[#c89b3c]"
type="checkbox"
checked={remember}
disabled={disabled}
onChange={event => setRemember(event.target.checked)}
/>
<span></span>
</label>
{status === 'success' ? <span className="login-success"><Icon type="check" /> </span> : null}
</div>
{message ? <div className="login-message">{message}</div> : null}
{status === 'success' ? (
<div className="mt-3">
<div className="inline-flex h-9 w-9 items-center justify-center rounded-[8px] border border-emerald-300/30 bg-emerald-400/10 text-emerald-100">
<Icon type="check" className="h-4 w-4" />
</div>
</div>
) : null}
<button className="login-submit" type="submit" disabled={disabled}>
<Icon type="arrow" />
{status === 'loading' ? '验证中' : '进入'}
</button>
</form>
</section>
<button
className="mt-4 flex h-11 w-full items-center justify-center gap-2 rounded-[8px] bg-white px-4 text-base font-semibold text-black shadow-xl shadow-black/25 transition hover:bg-[#f5efe3] focus:outline-none focus:ring-2 focus:ring-[#d6b36a]/60 disabled:cursor-wait disabled:opacity-70"
type="submit"
disabled={disabled}
aria-label={status === 'loading' ? '验证中' : '进入'}
>
<Icon type="arrow" className="h-4 w-4" />
</button>
</form>
</section>
</div>
</main>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import { useEffect, useRef } from 'react';
export function OasisCanvas() {
const frameRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
const dispatchNativeMouseEvent = (type: 'mousemove' | 'mouseleave', event?: PointerEvent) => {
const frameWindow = frameRef.current?.contentWindow;
if (!frameWindow) return false;
try {
const nativeEvent = new MouseEvent(type, {
bubbles: true,
cancelable: false,
clientX: event?.clientX ?? 99999,
clientY: event?.clientY ?? 99999,
screenX: event?.screenX ?? 99999,
screenY: event?.screenY ?? 99999,
buttons: event?.buttons ?? 0,
view: frameWindow,
});
frameWindow.dispatchEvent(nativeEvent);
return true;
} catch {
return false;
}
};
const sendPointer = (type: 'pointermove' | 'pointerleave', event?: PointerEvent) => {
const frameWindow = frameRef.current?.contentWindow;
if (!frameWindow) return;
dispatchNativeMouseEvent(type === 'pointermove' ? 'mousemove' : 'mouseleave', event);
frameWindow.postMessage(
{
type: `skg-oasis-${type}`,
x: event?.clientX ?? 99999,
y: event?.clientY ?? 99999,
},
window.location.origin,
);
};
const onPointerMove = (event: PointerEvent) => sendPointer('pointermove', event);
const onPointerLeave = () => sendPointer('pointerleave');
const listenerOptions = { capture: true, passive: true };
document.addEventListener('pointermove', onPointerMove, listenerOptions);
window.addEventListener('pointerleave', onPointerLeave, listenerOptions);
return () => {
document.removeEventListener('pointermove', onPointerMove, listenerOptions);
window.removeEventListener('pointerleave', onPointerLeave, listenerOptions);
};
}, []);
return (
<iframe
ref={frameRef}
allow="webgpu; fullscreen"
aria-hidden="true"
className="login-oasis-canvas"
loading="eager"
src="/oasis-source/index.html?v=login-pointer-0515"
/>
);
}