feat: 添加个人设置页面,支持修改个人信息和密码

This commit is contained in:
菜鸟生产 2026-03-18 12:49:15 +08:00
parent f82028562d
commit 5ca8eb5ba6
4 changed files with 297 additions and 3 deletions

View File

@ -1,9 +1,9 @@
# 认证路由 - 使用字段编码 # 认证路由 - 使用字段编码
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Body
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.core.auth import verify_password, create_access_token, get_password_hash from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user
from app.models.models import User from app.models.models import User
from app.schemas.schemas import Token, UserCreate, UserResponse from app.schemas.schemas import Token, UserCreate, UserResponse
@ -94,3 +94,27 @@ def get_current_user_info(
status_code=status.HTTP_501_NOT_IMPLEMENTED, status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="请使用正确的依赖注入" detail="请使用正确的依赖注入"
) )
@router.post("/change-password")
def change_password(
old_password: str = Body(...),
new_password: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""修改当前用户密码"""
from app.core.auth import verify_password, get_password_hash
# 验证旧密码
if not verify_password(old_password, current_user.password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前密码错误"
)
# 更新密码
current_user.password = get_password_hash(new_password)
db.commit()
return {"message": "密码修改成功"}

View File

@ -2,7 +2,7 @@
# Version Configuration for Zodiac Collection Management System # Version Configuration for Zodiac Collection Management System
# 当前版本号 (语义化版本:主版本。次版本.修订版) # 当前版本号 (语义化版本:主版本。次版本.修订版)
VERSION=1.0.7 VERSION=1.0.8
# 版本代号 (可选) # 版本代号 (可选)
VERSION_CODENAME="新生" VERSION_CODENAME="新生"

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import Home from './pages/Home' import Home from './pages/Home'
import Settings from './pages/Settings'
import List from './pages/List' import List from './pages/List'
import Add from './pages/Add' import Add from './pages/Add'
import Stats from './pages/Stats' import Stats from './pages/Stats'
@ -31,6 +32,7 @@ export default function App() {
if (basePath === '/list') return <List /> if (basePath === '/list') return <List />
if (basePath === '/add') return <Add /> if (basePath === '/add') return <Add />
if (basePath === '/login') return <Login /> if (basePath === '/login') return <Login />
if (basePath === '/settings') return <Settings />
if (basePath === '/admin') return <Admin /> if (basePath === '/admin') return <Admin />
if (basePath.startsWith('/edit')) return <Edit /> if (basePath.startsWith('/edit')) return <Edit />
if (basePath.startsWith('/detail')) return <Detail /> if (basePath.startsWith('/detail')) return <Detail />

View File

@ -0,0 +1,268 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
export default function Settings() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [form, setForm] = useState({
username: '',
email: '',
phone: '',
bio: ''
})
const [passwordForm, setPasswordForm] = useState({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const navigate = useNavigate()
const token = localStorage.getItem('token')
useEffect(() => {
fetchUserInfo()
}, [])
const fetchUserInfo = async () => {
try {
const res = await fetch('/api/users/me', {
headers: { 'Authorization': 'Bearer ' + token }
})
if (res.ok) {
const data = await res.json()
setForm({
username: data.username || '',
email: data.email || '',
phone: data.phone || '',
bio: data.bio || ''
})
}
} catch (e) {
setError('获取用户信息失败')
} finally {
setLoading(false)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setSuccess('')
setSaving(true)
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(form)
})
if (res.ok) {
setSuccess('保存成功!')
//
const userStr = localStorage.getItem('user')
if (userStr) {
const user = JSON.parse(userStr)
user.username = form.username
localStorage.setItem('user', JSON.stringify(user))
}
} else {
const data = await res.json()
setError(data.error?.message || '保存失败')
}
} catch (e) {
setError('保存失败,请重试')
} finally {
setSaving(false)
}
}
const handlePasswordChange = async (e) => {
e.preventDefault()
setError('')
setSuccess('')
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setError('两次输入的密码不一致')
return
}
if (passwordForm.newPassword.length < 6) {
setError('密码长度至少6位')
return
}
setSaving(true)
try {
const res = await fetch('/api/auth/change-password', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
old_password: passwordForm.oldPassword,
new_password: passwordForm.newPassword
})
})
if (res.ok) {
setSuccess('密码修改成功!')
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' })
} else {
const data = await res.json()
setError(data.error?.message || '密码修改失败')
}
} catch (e) {
setError('密码修改失败,请重试')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div style={{ minHeight: '100vh', background: '#0f172a', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ color: '#fff' }}>加载中...</div>
</div>
)
}
return (
<div style={{ minHeight: '100vh', background: '#0f172a', padding: '20px', paddingBottom: '80px' }}>
{/* 顶部导航 */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '24px' }}>
<div onClick={() => navigate('/')} style={{ cursor: 'pointer', fontSize: '20px' }}></div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginLeft: '16px' }}>个人设置</div>
</div>
{error && <div style={{ background: '#fee2e2', color: '#dc2626', padding: '12px', borderRadius: '8px', marginBottom: '16px' }}>{error}</div>}
{success && <div style={{ background: '#dcfce7', color: '#16a34a', padding: '12px', borderRadius: '8px', marginBottom: '16px' }}>{success}</div>}
{/* 基本信息 */}
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>基本信息</div>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>用户名</label>
<input
type="text"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>邮箱</label>
<input
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>手机号</label>
<input
type="tel"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>简介</label>
<textarea
value={form.bio}
onChange={(e) => setForm({ ...form, bio: e.target.value })}
rows={3}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff', resize: 'none' }}
/>
</div>
<button
type="submit"
disabled={saving}
style={{ width: '100%', padding: '14px', borderRadius: '8px', border: 'none', background: '#3b82f6', color: '#fff', fontSize: '16px', cursor: saving ? 'not-allowed' : 'pointer', opacity: saving ? 0.6 : 1 }}
>
{saving ? '保存中...' : '保存修改'}
</button>
</form>
</div>
{/* 修改密码 */}
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>修改密码</div>
<form onSubmit={handlePasswordChange}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>当前密码</label>
<input
type="password"
value={passwordForm.oldPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
placeholder="请输入当前密码"
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>新密码</label>
<input
type="password"
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
placeholder="至少6位"
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>确认新密码</label>
<input
type="password"
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
placeholder="再次输入新密码"
/>
</div>
<button
type="submit"
disabled={saving}
style={{ width: '100%', padding: '14px', borderRadius: '8px', border: 'none', background: '#f59e0b', color: '#fff', fontSize: '16px', cursor: saving ? 'not-allowed' : 'pointer', opacity: saving ? 0.6 : 1 }}
>
{saving ? '修改中...' : '修改密码'}
</button>
</form>
</div>
{/* 退出登录 */}
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px' }}>
<button
onClick={() => {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.hash = '#/login'
}}
style={{ width: '100%', padding: '14px', borderRadius: '8px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', fontSize: '16px', cursor: 'pointer' }}
>
退出登录
</button>
</div>
</div>
)
}