feat(users): add comprehensive user management with search, filtering, and admin controls

- Add paginated user list endpoint with search by email, name, and phone
- Implement role-based and status filtering for user queries
- Add total count to user list response for pagination UI
- Create new Account page component for user profile management
- Add PUT /me endpoint for users to update their own profile information
- Add GET /{user_id} endpoint to retrieve specific user details
- Add PUT /{user_id} endpoint for admin user updates with full control
- Add DELETE /{user_id} endpoint for admin user deletion with self-protection
- Add POST /{user_id}/toggle-status endpoint to enable/disable user accounts
- Implement admin-only access control on user creation and management endpoints
- Add phone field support to user schema and creation flow
- Update user list response schema with pagination metadata
- Improve query validation with min/max constraints on pagination parameters
- Add Chinese localization to user-facing error messages and docstrings
- Update frontend routes to include new Account page
- Update Sidebar navigation to support account management links
- Update Login page styling to match new UI design system
This commit is contained in:
lintsinghua 2025-11-28 01:06:01 +08:00
parent 7aa4ab89cb
commit 5676211b20
6 changed files with 716 additions and 75 deletions

View File

@ -1,54 +1,96 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException
from typing import Any, List, Optional
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from fastapi.encoders import jsonable_encoder
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import func, or_
from app.api import deps
from app.core import security
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate, UserListResponse
router = APIRouter()
@router.get("/", response_model=List[UserSchema])
@router.get("/", response_model=UserListResponse)
async def read_users(
db: AsyncSession = Depends(get_db),
skip: int = 0,
limit: int = 100,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
search: Optional[str] = Query(None, description="搜索关键词"),
role: Optional[str] = Query(None, description="角色筛选"),
is_active: Optional[bool] = Query(None, description="状态筛选"),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Retrieve users.
获取用户列表支持分页搜索筛选
"""
result = await db.execute(select(User).offset(skip).limit(limit))
query = select(User)
count_query = select(func.count(User.id))
# 搜索条件
if search:
search_filter = or_(
User.email.ilike(f"%{search}%"),
User.full_name.ilike(f"%{search}%"),
User.phone.ilike(f"%{search}%")
)
query = query.where(search_filter)
count_query = count_query.where(search_filter)
# 角色筛选
if role:
query = query.where(User.role == role)
count_query = count_query.where(User.role == role)
# 状态筛选
if is_active is not None:
query = query.where(User.is_active == is_active)
count_query = count_query.where(User.is_active == is_active)
# 获取总数
total_result = await db.execute(count_query)
total = total_result.scalar()
# 分页查询
query = query.order_by(User.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(query)
users = result.scalars().all()
return users
return {
"users": users,
"total": total,
"skip": skip,
"limit": limit
}
@router.post("/", response_model=UserSchema)
async def create_user(
*,
db: AsyncSession = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new user.
创建新用户仅管理员
"""
result = await db.execute(select(User).where(User.email == user_in.email))
user = result.scalars().first()
if user:
raise HTTPException(
status_code=400,
detail="The user with this username already exists in the system.",
detail="该邮箱已被注册",
)
db_user = User(
email=user_in.email,
hashed_password=security.get_password_hash(user_in.password),
full_name=user_in.full_name,
is_active=user_in.is_active,
is_superuser=user_in.is_superuser,
phone=user_in.phone,
role=user_in.role,
is_active=user_in.is_active if user_in.is_active is not None else True,
is_superuser=user_in.is_superuser if user_in.is_superuser is not None else False,
)
db.add(db_user)
await db.commit()
@ -60,8 +102,125 @@ async def read_user_me(
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Get current user.
获取当前用户信息
"""
return current_user
@router.put("/me", response_model=UserSchema)
async def update_user_me(
*,
db: AsyncSession = Depends(get_db),
user_in: UserUpdate,
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
更新当前用户信息
"""
update_data = user_in.model_dump(exclude_unset=True)
# 普通用户不能修改自己的角色和超级管理员状态
update_data.pop('role', None)
update_data.pop('is_superuser', None)
update_data.pop('is_active', None)
# 如果更新密码
if 'password' in update_data and update_data['password']:
update_data['hashed_password'] = security.get_password_hash(update_data['password'])
update_data.pop('password', None)
for field, value in update_data.items():
setattr(current_user, field, value)
await db.commit()
await db.refresh(current_user)
return current_user
@router.get("/{user_id}", response_model=UserSchema)
async def read_user(
user_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
获取指定用户信息
"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return user
@router.put("/{user_id}", response_model=UserSchema)
async def update_user(
user_id: str,
*,
db: AsyncSession = Depends(get_db),
user_in: UserUpdate,
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
更新用户信息仅管理员
"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
update_data = user_in.model_dump(exclude_unset=True)
# 如果更新密码
if 'password' in update_data and update_data['password']:
update_data['hashed_password'] = security.get_password_hash(update_data['password'])
update_data.pop('password', None)
for field, value in update_data.items():
setattr(user, field, value)
await db.commit()
await db.refresh(user)
return user
@router.delete("/{user_id}")
async def delete_user(
user_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
删除用户仅管理员
"""
if user_id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除自己的账户")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
await db.delete(user)
await db.commit()
return {"message": "用户已删除"}
@router.post("/{user_id}/toggle-status", response_model=UserSchema)
async def toggle_user_status(
user_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
切换用户状态启用/禁用
"""
if user_id == current_user.id:
raise HTTPException(status_code=400, detail="不能禁用自己的账户")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user.is_active = not user.is_active
await db.commit()
await db.refresh(user)
return user

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
@ -33,4 +33,10 @@ class UserInDBBase(UserBase):
class User(UserInDBBase):
pass
class UserListResponse(BaseModel):
users: List[User]
total: int
skip: int
limit: int

View File

@ -7,6 +7,7 @@ import AuditTasks from "@/pages/AuditTasks";
import TaskDetail from "@/pages/TaskDetail";
import AdminDashboard from "@/pages/AdminDashboard";
import LogsPage from "@/pages/LogsPage";
import Account from "@/pages/Account";
import type { ReactNode } from 'react';
export interface RouteConfig {
@ -71,6 +72,12 @@ const routes: RouteConfig[] = [
element: <LogsPage />,
visible: true,
},
{
name: "账号管理",
path: "/account",
element: <Account />,
visible: false, // 不在主导航显示,在侧边栏底部单独显示
},
];
export default routes;

View File

@ -13,7 +13,8 @@ import {
FileText,
ChevronLeft,
ChevronRight,
Github
Github,
UserCircle
} from "lucide-react";
import routes from "@/app/routes";
@ -152,8 +153,30 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
</div>
</nav>
{/* Footer with GitHub Link */}
<div className="p-4 border-t border-border bg-card z-10">
{/* Footer with Account & GitHub Link */}
<div className="p-4 border-t border-border bg-card z-10 space-y-2">
{/* Account Link */}
<Link
to="/account"
className={`
flex items-center space-x-3 px-3 py-2.5 rounded-sm
transition-all duration-200 group
border
${location.pathname === '/account'
? "bg-primary border-primary/30 shadow-md text-white"
: "bg-transparent border-transparent hover:border-border hover:bg-card hover:shadow-sm text-gray-600 hover:text-foreground"
}
`}
onClick={() => setMobileOpen(false)}
title={collapsed ? "账号管理" : undefined}
>
<UserCircle className={`w-5 h-5 flex-shrink-0 ${location.pathname === '/account' ? 'text-white' : ''}`} />
{!collapsed && (
<span className="font-mono font-bold text-sm uppercase"></span>
)}
</Link>
{/* GitHub Link */}
<a
href="https://github.com/lintsinghua/XCodeReviewer"
target="_blank"

View File

@ -0,0 +1,363 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import {
User,
Mail,
Phone,
Shield,
Calendar,
Save,
Loader2,
KeyRound,
LogOut,
UserPlus,
GitBranch
} from "lucide-react";
import { apiClient } from "@/shared/api/serverClient";
import { toast } from "sonner";
import type { Profile } from "@/shared/types";
export default function Account() {
const navigate = useNavigate();
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const [form, setForm] = useState({
full_name: "",
phone: "",
github_username: "",
gitlab_username: "",
});
const [passwordForm, setPasswordForm] = useState({
current_password: "",
new_password: "",
confirm_password: "",
});
const [changingPassword, setChangingPassword] = useState(false);
useEffect(() => {
loadProfile();
}, []);
const loadProfile = async () => {
try {
setLoading(true);
const res = await apiClient.get('/users/me');
setProfile(res.data);
setForm({
full_name: res.data.full_name || "",
phone: res.data.phone || "",
github_username: res.data.github_username || "",
gitlab_username: res.data.gitlab_username || "",
});
} catch (error) {
console.error('Failed to load profile:', error);
toast.error("加载账号信息失败");
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
const res = await apiClient.put('/users/me', form);
setProfile(res.data);
toast.success("账号信息已更新");
} catch (error) {
console.error('Failed to update profile:', error);
toast.error("更新失败");
} finally {
setSaving(false);
}
};
const handleChangePassword = async () => {
if (!passwordForm.new_password || !passwordForm.confirm_password) {
toast.error("请填写新密码");
return;
}
if (passwordForm.new_password !== passwordForm.confirm_password) {
toast.error("两次输入的密码不一致");
return;
}
if (passwordForm.new_password.length < 6) {
toast.error("密码长度至少6位");
return;
}
try {
setChangingPassword(true);
await apiClient.put('/users/me', { password: passwordForm.new_password });
toast.success("密码已更新");
setPasswordForm({ current_password: "", new_password: "", confirm_password: "" });
} catch (error) {
console.error('Failed to change password:', error);
toast.error("密码更新失败");
} finally {
setChangingPassword(false);
}
};
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const getInitials = (name?: string, email?: string) => {
if (name) return name.charAt(0).toUpperCase();
if (email) return email.charAt(0).toUpperCase();
return "U";
};
const handleLogout = () => {
localStorage.removeItem('access_token');
toast.success("已退出登录");
navigate('/login');
};
const handleSwitchAccount = () => {
localStorage.removeItem('access_token');
navigate('/login');
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
return (
<div className="flex flex-col gap-6 px-6 pt-0 pb-4 bg-background min-h-screen font-mono relative overflow-hidden">
{/* Decorative Background */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
{/* Header */}
<div className="relative z-10 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b-4 border-black pb-6 bg-white/50 backdrop-blur-sm p-4 retro-border">
<div>
<h1 className="text-3xl font-display font-bold text-black uppercase tracking-tighter">
<span className="text-primary">_管理</span>
</h1>
<p className="text-gray-600 mt-1 font-mono border-l-2 border-primary pl-2"></p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={handleSwitchAccount}
className="terminal-btn-primary bg-white text-black hover:bg-gray-100"
>
<UserPlus className="w-4 h-4 mr-2" />
</Button>
<Button
variant="destructive"
onClick={() => setShowLogoutDialog(true)}
className="bg-red-500 hover:bg-red-600 text-white border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
>
<LogOut className="w-4 h-4 mr-2" />
退
</Button>
</div>
</div>
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Profile Card */}
<Card className="retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<CardHeader className="text-center pb-2">
<Avatar className="w-24 h-24 mx-auto border-4 border-black">
<AvatarImage src={profile?.avatar_url} />
<AvatarFallback className="bg-primary text-white text-2xl font-bold">
{getInitials(profile?.full_name, profile?.email)}
</AvatarFallback>
</Avatar>
<CardTitle className="mt-4 font-display uppercase">{profile?.full_name || "未设置姓名"}</CardTitle>
<CardDescription className="font-mono">{profile?.email}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Separator />
<div className="space-y-3 text-sm">
<div className="flex items-center gap-3">
<Shield className="w-4 h-4 text-gray-500" />
<span className="text-gray-600">:</span>
<span className="font-bold uppercase">{profile?.role === 'admin' ? '管理员' : '成员'}</span>
</div>
<div className="flex items-center gap-3">
<Calendar className="w-4 h-4 text-gray-500" />
<span className="text-gray-600">:</span>
<span className="font-bold">{formatDate(profile?.created_at)}</span>
</div>
</div>
</CardContent>
</Card>
{/* Edit Form */}
<Card className="lg:col-span-2 retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<CardHeader>
<CardTitle className="font-display uppercase flex items-center gap-2">
<User className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email" className="font-mono font-bold uppercase text-xs flex items-center gap-2">
<Mail className="w-3 h-3" />
</Label>
<Input
id="email"
value={profile?.email || ""}
disabled
className="terminal-input bg-gray-100"
/>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label htmlFor="full_name" className="font-mono font-bold uppercase text-xs flex items-center gap-2">
<User className="w-3 h-3" />
</Label>
<Input
id="full_name"
value={form.full_name}
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
placeholder="请输入姓名"
className="terminal-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone" className="font-mono font-bold uppercase text-xs flex items-center gap-2">
<Phone className="w-3 h-3" />
</Label>
<Input
id="phone"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="请输入手机号"
className="terminal-input"
/>
</div>
</div>
<Separator />
<div className="space-y-4">
<h3 className="font-display font-bold uppercase text-sm"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="github" className="font-mono font-bold uppercase text-xs flex items-center gap-2">
<GitBranch className="w-3 h-3" /> GitHub
</Label>
<Input
id="github"
value={form.github_username}
onChange={(e) => setForm({ ...form, github_username: e.target.value })}
placeholder="your-github-username"
className="terminal-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="gitlab" className="font-mono font-bold uppercase text-xs flex items-center gap-2">
<GitBranch className="w-3 h-3" /> GitLab
</Label>
<Input
id="gitlab"
value={form.gitlab_username}
onChange={(e) => setForm({ ...form, gitlab_username: e.target.value })}
placeholder="your-gitlab-username"
className="terminal-input"
/>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button onClick={handleSave} disabled={saving} className="terminal-btn-primary">
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
</Button>
</div>
</CardContent>
</Card>
{/* Password Change */}
<Card className="lg:col-span-3 retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<CardHeader>
<CardTitle className="font-display uppercase flex items-center gap-2">
<KeyRound className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="new_password" className="font-mono font-bold uppercase text-xs"></Label>
<Input
id="new_password"
type="password"
value={passwordForm.new_password}
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
placeholder="输入新密码"
className="terminal-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm_password" className="font-mono font-bold uppercase text-xs"></Label>
<Input
id="confirm_password"
type="password"
value={passwordForm.confirm_password}
onChange={(e) => setPasswordForm({ ...passwordForm, confirm_password: e.target.value })}
placeholder="再次输入新密码"
className="terminal-input"
/>
</div>
<div className="flex items-end">
<Button onClick={handleChangePassword} disabled={changingPassword} variant="outline" className="terminal-btn-primary bg-white text-black hover:bg-gray-100">
{changingPassword ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <KeyRound className="w-4 h-4 mr-2" />}
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Logout Confirmation Dialog */}
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<AlertDialogContent className="retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<AlertDialogHeader>
<AlertDialogTitle className="font-display uppercase">退</AlertDialogTitle>
<AlertDialogDescription className="font-mono">
退访
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="terminal-btn-primary bg-white text-black hover:bg-gray-100">
</AlertDialogCancel>
<AlertDialogAction
onClick={handleLogout}
className="bg-red-500 hover:bg-red-600 text-white border-2 border-black"
>
退
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@ -1,25 +1,34 @@
import { useState, FormEvent, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/shared/context/AuthContext';
import { apiClient } from '@/shared/api/serverClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { Lock, Cpu } from 'lucide-react';
import { useState, FormEvent, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "@/shared/context/AuthContext";
import { apiClient } from "@/shared/api/serverClient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { toast } from "sonner";
import { Lock, Mail, Terminal, Shield, Fingerprint } from "lucide-react";
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { login, isAuthenticated } = useAuth();
const from = location.state?.from?.pathname || '/';
const from = location.state?.from?.pathname || "/";
useEffect(() => {
// 检查是否有保存的邮箱
const savedEmail = localStorage.getItem("remembered_email");
if (savedEmail) {
setEmail(savedEmail);
setRememberMe(true);
}
}, []);
// 监听认证状态,登录成功后自动跳转
useEffect(() => {
if (isAuthenticated && !loading) {
navigate(from, { replace: true });
@ -31,79 +40,118 @@ export default function Login() {
setLoading(true);
try {
const formData = new URLSearchParams();
formData.append('username', email);
formData.append('password', password);
formData.append("username", email);
formData.append("password", password);
const response = await apiClient.post('/auth/login', formData, {
const response = await apiClient.post("/auth/login", formData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
"Content-Type": "application/x-www-form-urlencoded",
},
});
// 记住邮箱
if (rememberMe) {
localStorage.setItem("remembered_email", email);
} else {
localStorage.removeItem("remembered_email");
}
await login(response.data.access_token);
toast.success('访问已授予');
// 跳转由 useEffect 监听 isAuthenticated 状态变化自动处理
toast.success("登录成功");
} catch (error: any) {
toast.error(error.response?.data?.detail || '访问被拒绝');
toast.error(error.response?.data?.detail || "登录失败,请检查邮箱和密码");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-background relative overflow-hidden">
{/* Decorative Background Elements */}
<div className="min-h-screen flex items-center justify-center bg-gray-50 relative overflow-hidden">
{/* Background Grid */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
<div className="absolute top-10 left-10 font-mono text-xs text-gray-400 hidden md:block">
<div>系统ID: 0x84F2</div>
<div>状态: 等待输入</div>
<div>加密: AES-256</div>
{/* Decorative Elements */}
<div className="absolute top-8 left-8 font-mono text-[10px] text-gray-400 hidden lg:block space-y-1">
<div className="flex items-center gap-2">
<Terminal className="w-3 h-3" />
<span>SYS_ID: 0x84F2</span>
</div>
<div className="flex items-center gap-2">
<Shield className="w-3 h-3" />
<span>ENCRYPT: AES-256</span>
</div>
<div className="flex items-center gap-2">
<Fingerprint className="w-3 h-3" />
<span>AUTH: READY</span>
</div>
</div>
<div className="absolute bottom-10 right-10 font-mono text-xs text-gray-400 hidden md:block text-right">
<div></div>
<div>端口: 443</div>
<div className="absolute bottom-8 right-8 font-mono text-[10px] text-gray-400 hidden lg:block text-right space-y-1">
<div>SECURE_CONN: TRUE</div>
<div>PORT: 443</div>
<div>TLS: 1.3</div>
</div>
{/* Main Card */}
<div className="w-full max-w-md relative z-10 p-4">
<div className="w-full max-w-md relative z-10 px-4">
{/* Logo & Title */}
<div className="mb-8 text-center">
<div className="inline-flex items-center justify-center p-4 bg-white border border-border shadow-md mb-4 rounded-sm">
<img src="/logo_xcodereviewer.png" alt="XCodeReviewer" className="w-16 h-16 object-contain" />
<div className="inline-flex items-center justify-center p-3 bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] mb-6">
<img
src="/logo_xcodereviewer.png"
alt="XCodeReviewer"
className="w-14 h-14 object-contain"
/>
</div>
<h1 className="text-4xl font-display font-bold tracking-tighter uppercase">
<h1 className="text-3xl font-display font-bold tracking-tighter uppercase">
XCode<span className="text-primary">Reviewer</span>
</h1>
<p className="text-sm font-mono text-gray-500 mt-2"></p>
<p className="text-sm font-mono text-gray-500 mt-2">
// 代码审计平台
</p>
</div>
<div className="terminal-card p-8 relative">
{/* Decorative Corner Markers - Subtle */}
<div className="absolute top-3 left-3 w-1.5 h-1.5 bg-primary/20 rounded-sm" />
<div className="absolute top-3 right-3 w-1.5 h-1.5 bg-primary/20 rounded-sm" />
<div className="absolute bottom-3 left-3 w-1.5 h-1.5 bg-primary/20 rounded-sm" />
<div className="absolute bottom-3 right-3 w-1.5 h-1.5 bg-primary/20 rounded-sm" />
{/* Login Form Card */}
<div className="bg-white border-2 border-black shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] p-8 relative">
{/* Card Header */}
<div className="flex items-center gap-2 mb-6 pb-4 border-b-2 border-dashed border-gray-200">
<div className="w-3 h-3 rounded-full bg-red-400" />
<div className="w-3 h-3 rounded-full bg-yellow-400" />
<div className="w-3 h-3 rounded-full bg-green-400" />
<span className="ml-2 font-mono text-xs text-gray-500 uppercase">
</span>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-6 mt-4">
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<div className="space-y-2">
<Label htmlFor="email" className="font-mono uppercase text-xs font-bold"> / </Label>
<Label
htmlFor="email"
className="font-mono uppercase text-xs font-bold"
>
</Label>
<div className="relative">
<Input
id="email"
type="email"
placeholder="USER@DOMAIN.COM"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="terminal-input font-mono pl-10"
className="h-12 pl-11 font-mono border-2 border-gray-200 focus:border-black transition-colors"
/>
<Cpu className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="font-mono uppercase text-xs font-bold"></Label>
<Label
htmlFor="password"
className="font-mono uppercase text-xs font-bold"
>
</Label>
<div className="relative">
<Input
id="password"
@ -112,30 +160,65 @@ export default function Login() {
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="terminal-input font-mono pl-10"
className="h-12 pl-11 font-mono border-2 border-gray-200 focus:border-black transition-colors"
/>
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="remember"
checked={rememberMe}
onCheckedChange={(checked) =>
setRememberMe(checked as boolean)
}
className="border-2 border-gray-300 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
<Label
htmlFor="remember"
className="text-sm font-mono text-gray-600 cursor-pointer"
>
</Label>
</div>
</div>
<Button
type="submit"
className="w-full terminal-btn-primary text-lg h-12 mt-4"
className="w-full h-12 text-base font-bold uppercase tracking-wider border-2 border-black shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] transition-all bg-primary hover:bg-primary/90"
disabled={loading}
>
{loading ? (
<span className="flex items-center gap-2">
<span className="animate-spin">/</span> ...
<span className="animate-spin"></span> ...
</span>
) : '初始化会话'}
) : (
"登 录"
)}
</Button>
</form>
<div className="mt-6 pt-6 terminal-divider text-center">
<div className="text-xs font-mono text-gray-500">
访 <span className="text-primary font-bold cursor-pointer hover:underline" onClick={() => navigate('/register')}>访</span>
{/* Footer */}
<div className="mt-6 pt-5 border-t-2 border-dashed border-gray-200 text-center">
<p className="text-sm font-mono text-gray-500">
{" "}
<span
className="text-primary font-bold cursor-pointer hover:underline"
onClick={() => navigate("/register")}
>
</span>
</p>
</div>
</div>
{/* Version Info */}
<div className="mt-6 text-center">
<p className="font-mono text-[10px] text-gray-400 uppercase">
Version 1.3.4 · Secure Connection
</p>
</div>
</div>
</div>