Browse Source

feat: frontend login accepts phone/username, remove email from auth flow

master
dark 1 month ago
parent
commit
e31c274a64
  1. 71
      frontend/react-shadcn/pc/src/contexts/AuthContext.tsx
  2. 369
      frontend/react-shadcn/pc/src/pages/LoginPage.tsx
  3. 13
      frontend/react-shadcn/pc/src/types/index.ts

71
frontend/react-shadcn/pc/src/contexts/AuthContext.tsx

@ -7,7 +7,8 @@ interface AuthContextType {
token: string | null
isAuthenticated: boolean
isLoading: boolean
login: (email: string, password: string) => Promise<void>
login: (account: string, password: string) => Promise<void>
loginWithToken: (token: string) => void
logout: () => void
}
@ -37,29 +38,74 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setIsLoading(false)
}, [])
const login = async (email: string, password: string) => {
const login = async (account: string, password: string) => {
try {
const response = await apiClient.login({ email, password })
const response = await apiClient.login({ account, password })
if (response.success && response.token) {
setToken(response.token)
// Mock user data - in real app, fetch from API
const userData: User = {
id: 1,
username: email.split('@')[0],
email,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
// 解析 JWT 获取用户信息
try {
const payload = JSON.parse(atob(response.token.split('.')[1]))
const userData: User = {
id: payload.userId || 0,
username: payload.username || account,
email: '',
role: payload.role || 'user',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
} catch {
const userData: User = {
id: 0,
username: account,
email: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
}
} catch (error) {
throw error
}
}
const loginWithToken = (ssoToken: string) => {
localStorage.setItem('token', ssoToken)
setToken(ssoToken)
// 解析 JWT 获取用户信息
try {
const payload = JSON.parse(atob(ssoToken.split('.')[1]))
const userData: User = {
id: payload.userId || 0,
username: payload.username || '',
email: payload.email || '',
role: payload.role || 'user',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
} catch {
// JWT 解析失败,使用占位数据
const userData: User = {
id: 0,
username: 'SSO User',
email: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
}
}
const logout = () => {
apiClient.logout()
setToken(null)
@ -73,6 +119,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isAuthenticated: !!token,
isLoading,
login,
loginWithToken,
logout,
}

369
frontend/react-shadcn/pc/src/pages/LoginPage.tsx

@ -1,26 +1,56 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Mail, Lock, AlertCircle, Zap } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { UserRound, Lock, AlertCircle, Eye, EyeOff, ArrowRight, Shield, Cpu, BarChart3 } from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
import { apiClient } from '@/services/api'
import { Input } from '@/components/ui/Input'
import { Button } from '@/components/ui/Button'
import { Card } from '@/components/ui/Card'
export function LoginPage() {
const [email, setEmail] = useState('')
const [account, setAccount] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [ssoLoading, setSsoLoading] = useState(false)
const [mounted, setMounted] = useState(false)
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const { login } = useAuth()
useEffect(() => {
setMounted(true)
// 检查 SSO 错误参数
const ssoError = searchParams.get('error')
if (ssoError === 'sso_failed') {
setError('SSO 登录失败,请重试')
}
}, [searchParams])
const handleSSOLogin = async () => {
setSsoLoading(true)
setError('')
try {
const res = await apiClient.getSSOLoginUrl()
if (res.login_url) {
window.location.href = res.login_url
} else {
setError('获取 SSO 登录链接失败')
setSsoLoading(false)
}
} catch {
setError('SSO 服务不可用')
setSsoLoading(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setIsLoading(true)
try {
await login(email, password)
await login(account, password)
navigate('/dashboard')
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败')
@ -30,106 +60,283 @@ export function LoginPage() {
}
return (
<div className="min-h-screen flex items-center justify-center relative overflow-hidden grid-pattern">
{/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-sky-500/10 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }} />
</div>
<div className="min-h-screen flex bg-[oklch(0.10_0.01_240)]">
{/* Left Panel - Brand Showcase (visible from md breakpoint) */}
<div
className={`hidden md:flex md:w-[45%] lg:w-[55%] relative overflow-hidden flex-col justify-between p-8 lg:p-12 transition-all duration-700 ease-out ${
mounted ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-8'
}`}
>
{/* Gradient background */}
<div className="absolute inset-0 bg-gradient-to-br from-indigo-600 via-blue-600 to-sky-500" />
{/* Decorative grid lines */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-sky-500/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-purple-500/30 to-transparent" />
<div className="absolute top-0 bottom-0 left-0 w-px bg-gradient-to-b from-transparent via-sky-500/30 to-transparent" />
<div className="absolute top-0 bottom-0 right-0 w-px bg-gradient-to-b from-transparent via-purple-500/30 to-transparent" />
</div>
{/* Subtle pattern overlay */}
<div
className="absolute inset-0 opacity-[0.07]"
style={{
backgroundImage:
'radial-gradient(circle at 1px 1px, white 1px, transparent 0)',
backgroundSize: '32px 32px',
}}
/>
{/* Main content */}
<div className="relative z-10 w-full max-w-md px-4 animate-scale-in">
{/* Logo and title */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 mb-6 rounded-2xl bg-gradient-to-br from-sky-500 to-blue-600 glow-primary relative overflow-hidden">
<div className="absolute inset-0 stripe-pattern" />
<Zap className="h-10 w-10 text-white relative z-10" />
{/* Floating orbs */}
<div className="absolute top-20 right-20 w-48 lg:w-72 h-48 lg:h-72 bg-white/10 rounded-full blur-3xl animate-login-float" />
<div
className="absolute bottom-32 left-16 w-40 lg:w-56 h-40 lg:h-56 bg-sky-300/15 rounded-full blur-3xl animate-login-float"
style={{ animationDelay: '2s', animationDuration: '8s' }}
/>
<div
className="absolute top-1/2 right-1/3 w-32 lg:w-40 h-32 lg:h-40 bg-indigo-300/10 rounded-full blur-2xl animate-login-float"
style={{ animationDelay: '4s', animationDuration: '10s' }}
/>
{/* Content */}
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-white/20 backdrop-blur-sm flex items-center justify-center">
<Cpu className="h-5 w-5 text-white" />
</div>
<span className="text-xl font-bold text-white tracking-wider font-display">
BASE
</span>
</div>
<h1 className="text-4xl font-bold text-white font-display tracking-wide mb-2">
BASE<span className="text-sky-400">.</span>
</div>
<div className="relative z-10 max-w-lg">
<h1
className={`text-3xl lg:text-5xl font-bold text-white leading-tight mb-4 lg:mb-6 font-display tracking-wide transition-all duration-700 delay-200 ease-out ${
mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'
}`}
>
<br />
<span className="text-sky-200"></span>
</h1>
<p className="text-gray-500 font-body"></p>
<p
className={`text-base lg:text-lg text-blue-100/80 leading-relaxed mb-6 lg:mb-10 font-body transition-all duration-700 delay-300 ease-out ${
mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'
}`}
>
</p>
{/* Feature highlights */}
<div
className={`space-y-3 lg:space-y-4 transition-all duration-700 delay-500 ease-out ${
mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'
}`}
>
{[
{ icon: Shield, text: 'JWT 认证与权限管理' },
{ icon: BarChart3, text: '实时数据仪表盘' },
{ icon: Cpu, text: 'Go + React 全栈架构' },
].map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-white/10 backdrop-blur-sm flex items-center justify-center flex-shrink-0">
<Icon className="h-4 w-4 text-sky-200" />
</div>
<span className="text-blue-100/90 text-sm font-body">{text}</span>
</div>
))}
</div>
</div>
{/* Bottom */}
<div className="relative z-10">
<p className="text-blue-200/50 text-sm font-body">
&copy; 2026 Base System. All rights reserved.
</p>
</div>
</div>
{/* Right Panel - Login Form */}
<div className="flex-1 flex items-center justify-center px-4 py-8 sm:px-8 md:px-6 lg:px-8 relative overflow-hidden">
{/* Subtle background effects for right panel */}
<div className="absolute inset-0">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-indigo-500/[0.03] via-transparent to-sky-500/[0.03]" />
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage:
'linear-gradient(rgba(99,102,241,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(99,102,241,0.3) 1px, transparent 1px)',
backgroundSize: '48px 48px',
}}
/>
</div>
{/* Login form */}
<Card className="backdrop-blur-xl bg-gray-900/90 border-gray-800">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div
className={`relative z-10 w-full max-w-[420px] transition-all duration-700 delay-100 ease-out ${
mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
}`}
>
{/* Mobile logo - only visible below md */}
<div className="md:hidden text-center mb-6">
<div className="inline-flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-sky-500 flex items-center justify-center">
<Cpu className="h-5 w-5 text-white" />
</div>
<span className="text-xl font-bold text-white tracking-wider font-display">
BASE
</span>
</div>
</div>
{/* Form header */}
<div className="mb-6">
<h2 className="text-xl sm:text-2xl font-bold text-white font-display tracking-wide mb-1.5">
</h2>
<p className="text-gray-400 font-body text-sm">
</p>
</div>
{/* Login card */}
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] backdrop-blur-xl p-5 sm:p-6 lg:p-8 shadow-2xl shadow-black/20">
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="email"
label="邮箱地址"
placeholder="user@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
type="text"
label="手机号 / 用户名"
placeholder="请输入手机号或用户名"
value={account}
onChange={(e) => setAccount(e.target.value)}
required
leftIcon={<Mail className="h-4 w-4" />}
leftIcon={<UserRound className="h-4 w-4" />}
disabled={isLoading}
/>
<Input
type="password"
type={showPassword ? 'text' : 'password'}
label="密码"
placeholder="••••••••"
placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
leftIcon={<Lock className="h-4 w-4" />}
rightIcon={
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="cursor-pointer text-gray-500 hover:text-gray-300 transition-colors duration-200"
tabIndex={-1}
aria-label={showPassword ? '隐藏密码' : '显示密码'}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
}
disabled={isLoading}
/>
</div>
{error && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm font-body">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isLoading}
>
</Button>
<div className="text-center">
<p className="text-sm text-gray-500 font-body">
{' '}
<button
type="button"
className="text-sky-400 hover:text-sky-300 transition-colors font-medium"
{error && (
<div
className="flex items-center gap-2.5 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm font-body animate-scale-in"
role="alert"
>
</button>
</p>
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full !bg-gradient-to-r !from-indigo-500 !to-sky-500 hover:!from-indigo-400 hover:!to-sky-400 !border-indigo-500/30 !shadow-lg !shadow-indigo-500/20 cursor-pointer group"
isLoading={isLoading}
>
<span></span>
{!isLoading && (
<ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
)}
</Button>
</form>
{/* Divider */}
<div className="relative my-5">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/[0.06]" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-3 bg-[oklch(0.12_0.01_240)] text-gray-500 font-body">
SSO
</span>
</div>
</div>
</form>
</Card>
{/* Footer info */}
<div className="mt-8 text-center">
<p className="text-xs text-gray-600 font-body">
© 2026 Base System. All rights reserved.
</p>
{/* SSO Buttons */}
<div className="flex flex-col sm:flex-row gap-2.5">
<button
type="button"
className="flex items-center justify-center gap-2.5 h-11 flex-1 rounded-xl border border-white/[0.08] bg-white/[0.03] hover:bg-white/[0.07] transition-colors duration-200 cursor-pointer group"
aria-label="使用 Google 登录"
>
<svg className="h-5 w-5 flex-shrink-0" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
<span className="text-sm text-gray-300 font-body group-hover:text-white transition-colors duration-200">Google</span>
</button>
<button
type="button"
className="flex items-center justify-center gap-2.5 h-11 flex-1 rounded-xl border border-white/[0.08] bg-white/[0.03] hover:bg-white/[0.07] transition-colors duration-200 cursor-pointer group"
aria-label="使用 GitHub 登录"
>
<svg className="h-5 w-5 flex-shrink-0 text-gray-300 group-hover:text-white transition-colors duration-200" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" />
</svg>
<span className="text-sm text-gray-300 font-body group-hover:text-white transition-colors duration-200">GitHub</span>
</button>
<button
type="button"
onClick={handleSSOLogin}
disabled={ssoLoading}
className="flex items-center justify-center gap-2.5 h-11 flex-1 rounded-xl border border-white/[0.08] bg-white/[0.03] hover:bg-white/[0.07] transition-colors duration-200 cursor-pointer group disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="使用 XHY.GROUP 登录"
>
<svg className="h-5 w-5 flex-shrink-0" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="xhy-grad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#6366F1" />
<stop offset="100%" stopColor="#06B6D4" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="7" fill="url(#xhy-grad)" />
<path d="M8 10L13.5 16L8 22H11.5L15.5 17.2L19.5 22H23L17.5 16L23 10H19.5L15.5 14.8L11.5 10H8Z" fill="white" fillOpacity="0.95" />
<rect x="6" y="25" width="20" height="1.5" rx="0.75" fill="white" fillOpacity="0.4" />
</svg>
<span className="text-sm text-gray-300 font-body group-hover:text-white transition-colors duration-200">XHY.GROUP</span>
</button>
</div>
{/* Register link */}
<p className="text-center text-sm text-gray-400 font-body mt-5">
{' '}
<button
type="button"
className="text-indigo-400 hover:text-indigo-300 transition-colors duration-200 font-medium cursor-pointer"
>
</button>
</p>
</div>
{/* Mobile footer */}
<div className="md:hidden mt-6 text-center">
<p className="text-xs text-gray-600 font-body">
&copy; 2026 Base System. All rights reserved.
</p>
</div>
</div>
</div>
{/* Corner decorations */}
<div className="absolute top-0 left-0 w-24 h-24 border-l-2 border-t-2 border-sky-500/30 rounded-tl-3xl" />
<div className="absolute top-0 right-0 w-24 h-24 border-r-2 border-t-2 border-purple-500/30 rounded-tr-3xl" />
<div className="absolute bottom-0 left-0 w-24 h-24 border-l-2 border-b-2 border-purple-500/30 rounded-bl-3xl" />
<div className="absolute bottom-0 right-0 w-24 h-24 border-r-2 border-b-2 border-sky-500/30 rounded-br-3xl" />
</div>
)
}

13
frontend/react-shadcn/pc/src/types/index.ts

@ -20,6 +20,9 @@ export interface User {
email: string
phone?: string
avatar?: string
role?: string
source?: string
remark?: string
createdAt: string
updatedAt: string
}
@ -28,15 +31,15 @@ export interface UserInfo extends User {}
// Request Types
export interface LoginRequest {
email: string
account: string
password: string
}
export interface RegisterRequest {
username: string
email: string
password: string
phone?: string
phone: string
email?: string
}
export interface CreateUserRequest {
@ -44,6 +47,8 @@ export interface CreateUserRequest {
email: string
password: string
phone?: string
role?: string
remark?: string
}
export interface UpdateUserRequest {
@ -51,6 +56,8 @@ export interface UpdateUserRequest {
email?: string
phone?: string
avatar?: string
role?: string
remark?: string
}
export interface UserListRequest {

Loading…
Cancel
Save