feat(login): redesign login page with split layout

This commit is contained in:
Supra4E8C
2026-01-31 14:59:00 +08:00
parent 96307873c5
commit c93030370e
3 changed files with 350 additions and 188 deletions

View File

@@ -6,6 +6,8 @@ import { Input } from '@/components/ui/Input';
import { IconEye, IconEyeOff } from '@/components/ui/icons';
import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores';
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
import styles from './LoginPage.module.scss';
export function LoginPage() {
const { t } = useTranslation();
@@ -50,7 +52,7 @@ export function LoginPage() {
init();
}, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]);
const handleSubmit = async () => {
const handleSubmit = useCallback(async () => {
if (!managementKey.trim()) {
setError(t('login.error_required'));
return;
@@ -74,7 +76,7 @@ export function LoginPage() {
} finally {
setLoading(false);
}
};
}, [apiBase, detectedBase, login, managementKey, navigate, rememberPassword, showNotification, t]);
const handleSubmitKeyDown = useCallback(
(event: React.KeyboardEvent) => {
@@ -92,103 +94,121 @@ export function LoginPage() {
}
return (
<div className="login-page">
<div className="login-card">
<div className="login-header">
<div className="login-title-row">
<div className="title">{t('title.login')}</div>
<Button
type="button"
variant="ghost"
size="sm"
className="login-language-btn"
onClick={toggleLanguage}
title={t('language.switch')}
aria-label={t('language.switch')}
>
{nextLanguageLabel}
<div className={styles.container}>
{/* 左侧品牌展示区 */}
<div className={styles.brandPanel}>
<div className={styles.brandContent}>
<span className={styles.brandWord}>CLI</span>
<span className={styles.brandWord}>PROXY</span>
<span className={styles.brandWord}>API</span>
</div>
</div>
{/* 右侧功能交互区 */}
<div className={styles.formPanel}>
<div className={styles.formContent}>
{/* Logo */}
<img src={INLINE_LOGO_JPEG} alt="Logo" className={styles.logo} />
{/* 登录表单卡片 */}
<div className={styles.loginCard}>
<div className={styles.loginHeader}>
<div className={styles.titleRow}>
<div className={styles.title}>{t('title.login')}</div>
<Button
type="button"
variant="ghost"
size="sm"
className={styles.languageBtn}
onClick={toggleLanguage}
title={t('language.switch')}
aria-label={t('language.switch')}
>
{nextLanguageLabel}
</Button>
</div>
<div className={styles.subtitle}>{t('login.subtitle')}</div>
</div>
<div className={styles.connectionBox}>
<div className={styles.label}>{t('login.connection_current')}</div>
<div className={styles.value}>{apiBase || detectedBase}</div>
<div className={styles.hint}>{t('login.connection_auto_hint')}</div>
</div>
<div className={styles.toggleAdvanced}>
<input
id="custom-connection-toggle"
type="checkbox"
checked={showCustomBase}
onChange={(e) => setShowCustomBase(e.target.checked)}
/>
<label htmlFor="custom-connection-toggle">{t('login.custom_connection_label')}</label>
</div>
{showCustomBase && (
<Input
label={t('login.custom_connection_label')}
placeholder={t('login.custom_connection_placeholder')}
value={apiBase}
onChange={(e) => setApiBase(e.target.value)}
hint={t('login.custom_connection_hint')}
/>
)}
<Input
autoFocus
label={t('login.management_key_label')}
placeholder={t('login.management_key_placeholder')}
type={showKey ? 'text' : 'password'}
value={managementKey}
onChange={(e) => setManagementKey(e.target.value)}
onKeyDown={handleSubmitKeyDown}
rightElement={
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => setShowKey((prev) => !prev)}
aria-label={
showKey
? t('login.hide_key', { defaultValue: '隐藏密钥' })
: t('login.show_key', { defaultValue: '显示密钥' })
}
title={
showKey
? t('login.hide_key', { defaultValue: '隐藏密钥' })
: t('login.show_key', { defaultValue: '显示密钥' })
}
>
{showKey ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</button>
}
/>
<div className={styles.toggleAdvanced}>
<input
id="remember-password-toggle"
type="checkbox"
checked={rememberPassword}
onChange={(e) => setRememberPassword(e.target.checked)}
/>
<label htmlFor="remember-password-toggle">{t('login.remember_password_label')}</label>
</div>
<Button fullWidth onClick={handleSubmit} loading={loading}>
{loading ? t('login.submitting') : t('login.submit_button')}
</Button>
{error && <div className={styles.errorBox}>{error}</div>}
{autoLoading && (
<div className={styles.autoLoginBox}>
<div className={styles.label}>{t('auto_login.title')}</div>
<div className={styles.value}>{t('auto_login.message')}</div>
</div>
)}
</div>
<div className="subtitle">{t('login.subtitle')}</div>
</div>
<div className="connection-box">
<div className="label">{t('login.connection_current')}</div>
<div className="value">{apiBase || detectedBase}</div>
<div className="hint">{t('login.connection_auto_hint')}</div>
</div>
<div className="toggle-advanced">
<input
id="custom-connection-toggle"
type="checkbox"
checked={showCustomBase}
onChange={(e) => setShowCustomBase(e.target.checked)}
/>
<label htmlFor="custom-connection-toggle">{t('login.custom_connection_label')}</label>
</div>
{showCustomBase && (
<Input
label={t('login.custom_connection_label')}
placeholder={t('login.custom_connection_placeholder')}
value={apiBase}
onChange={(e) => setApiBase(e.target.value)}
hint={t('login.custom_connection_hint')}
/>
)}
<Input
autoFocus
label={t('login.management_key_label')}
placeholder={t('login.management_key_placeholder')}
type={showKey ? 'text' : 'password'}
value={managementKey}
onChange={(e) => setManagementKey(e.target.value)}
onKeyDown={handleSubmitKeyDown}
rightElement={
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => setShowKey((prev) => !prev)}
aria-label={
showKey
? t('login.hide_key', { defaultValue: '隐藏密钥' })
: t('login.show_key', { defaultValue: '显示密钥' })
}
title={
showKey
? t('login.hide_key', { defaultValue: '隐藏密钥' })
: t('login.show_key', { defaultValue: '显示密钥' })
}
>
{showKey ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</button>
}
/>
<div className="toggle-advanced">
<input
id="remember-password-toggle"
type="checkbox"
checked={rememberPassword}
onChange={(e) => setRememberPassword(e.target.checked)}
/>
<label htmlFor="remember-password-toggle">{t('login.remember_password_label')}</label>
</div>
<Button fullWidth onClick={handleSubmit} loading={loading}>
{loading ? t('login.submitting') : t('login.submit_button')}
</Button>
{error && <div className="error-box">{error}</div>}
{autoLoading && (
<div className="connection-box">
<div className="label">{t('auto_login.title')}</div>
<div className="value">{t('auto_login.message')}</div>
</div>
)}
</div>
</div>
);