feat: add avatar

This commit is contained in:
qixinbo
2026-03-29 17:47:28 +08:00
parent 7d72f31b5f
commit caed4e086f
6 changed files with 57 additions and 6 deletions
+3
View File
@@ -49,6 +49,7 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
"id": user.id, "id": user.id,
"username": user.username, "username": user.username,
"email": user.email, "email": user.email,
"avatar": user.avatar,
"is_admin": user.is_admin "is_admin": user.is_admin
} }
} }
@@ -73,6 +74,7 @@ def register_user(user: UserCreate, background_tasks: BackgroundTasks, db: Sessi
db_user = User( db_user = User(
username=user.username, username=user.username,
email=user.email, email=user.email,
avatar=user.avatar,
hashed_password=hashed_password, hashed_password=hashed_password,
is_active=is_active, is_active=is_active,
is_admin=is_admin is_admin=is_admin
@@ -178,6 +180,7 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = User( db_user = User(
username=user.username, username=user.username,
email=user.email, email=user.email,
avatar=user.avatar,
hashed_password=get_password_hash(user.password), hashed_password=get_password_hash(user.password),
is_active=user.is_active, is_active=user.is_active,
is_admin=user.is_admin is_admin=user.is_admin
+1
View File
@@ -10,6 +10,7 @@ class User(Base):
username = Column(String, unique=True, index=True, nullable=False) username = Column(String, unique=True, index=True, nullable=False)
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False) hashed_password = Column(String, nullable=False)
avatar = Column(String, nullable=True) # Store avatar identifier or URL
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False) is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
+2
View File
@@ -5,6 +5,7 @@ from datetime import datetime
class UserBase(BaseModel): class UserBase(BaseModel):
username: str username: str
email: str email: str
avatar: Optional[str] = None
is_active: Optional[bool] = True is_active: Optional[bool] = True
is_admin: Optional[bool] = False is_admin: Optional[bool] = False
@@ -14,6 +15,7 @@ class UserCreate(UserBase):
class UserUpdate(BaseModel): class UserUpdate(BaseModel):
username: Optional[str] = None username: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
avatar: Optional[str] = None
is_active: Optional[bool] = None is_active: Optional[bool] = None
is_admin: Optional[bool] = None is_admin: Optional[bool] = None
password: Optional[str] = None password: Optional[str] = None
+6 -2
View File
@@ -885,8 +885,12 @@ function SidebarBody() {
} }
}} }}
> >
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm"> <div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm overflow-hidden">
<User className="h-4.5 w-4.5" /> {user?.avatar ? (
<img src={user.avatar} alt="avatar" className="w-full h-full object-cover" />
) : (
<User className="h-4.5 w-4.5" />
)}
</div> </div>
<div className="text-sm font-medium truncate max-w-[100px] text-left"> <div className="text-sm font-medium truncate max-w-[100px] text-left">
{user?.username || t('defaultUser')} {user?.username || t('defaultUser')}
+44 -4
View File
@@ -4,14 +4,30 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Save, Loader2 } from "lucide-react"; import { Save, Loader2, Check } from "lucide-react";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useAuthStore } from "@/store/authStore"; import { useAuthStore } from "@/store/authStore";
const BUILTIN_AVATARS = [
"https://api.dicebear.com/7.x/bottts/svg?seed=Felix",
"https://api.dicebear.com/7.x/bottts/svg?seed=Aneka",
"https://api.dicebear.com/7.x/bottts/svg?seed=Tinkerbell",
"https://api.dicebear.com/7.x/bottts/svg?seed=Bella",
"https://api.dicebear.com/7.x/bottts/svg?seed=Buster",
"https://api.dicebear.com/7.x/bottts/svg?seed=Max",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Leo",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Oliver",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Mia",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Lily",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Chloe",
"https://api.dicebear.com/7.x/adventurer/svg?seed=Simba"
];
export function Settings() { export function Settings() {
const { t } = useTranslation(); const { t } = useTranslation();
const { user, updateUser } = useAuthStore(); const { user, updateUser } = useAuthStore();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [avatar, setAvatar] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@@ -21,6 +37,7 @@ export function Settings() {
useEffect(() => { useEffect(() => {
if (user) { if (user) {
setEmail(user.email || ''); setEmail(user.email || '');
setAvatar(user.avatar || '');
} }
}, [user]); }, [user]);
@@ -38,7 +55,8 @@ export function Settings() {
setIsSaving(true); setIsSaving(true);
try { try {
const updateData: any = { const updateData: any = {
email: email email: email,
avatar: avatar || null
}; };
if (password) { if (password) {
@@ -55,8 +73,8 @@ export function Settings() {
setPassword(''); setPassword('');
setConfirmPassword(''); setConfirmPassword('');
// Update global state with new email // Update global state with new email and avatar
updateUser({ email: response.email }); updateUser({ email: response.email, avatar: response.avatar });
} }
} catch (error: any) { } catch (error: any) {
console.error("Failed to save settings", error); console.error("Failed to save settings", error);
@@ -87,6 +105,28 @@ export function Settings() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('avatar', 'Avatar')}</Label>
<div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-12 gap-2 mt-2">
{BUILTIN_AVATARS.map((url) => (
<div
key={url}
className={`relative cursor-pointer rounded-full overflow-hidden border-2 transition-all ${
avatar === url ? 'border-indigo-500 scale-110 shadow-md' : 'border-transparent hover:border-indigo-200'
}`}
onClick={() => setAvatar(url)}
>
<img src={url} alt="avatar" className="w-full h-auto bg-muted/30" />
{avatar === url && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<Check className="h-4 w-4 text-indigo-600 drop-shadow-sm" />
</div>
)}
</div>
))}
</div>
</div>
<div className="space-y-2 pt-2 border-t border-border">
<Label htmlFor="username">{t('username')}</Label> <Label htmlFor="username">{t('username')}</Label>
<Input <Input
id="username" id="username"
+1
View File
@@ -4,6 +4,7 @@ export interface User {
id: number; id: number;
username: string; username: string;
email: string; email: string;
avatar?: string | null;
is_admin: boolean; is_admin: boolean;
} }