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 sqlalchemy.orm import Session
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.schemas.schemas import Token, UserCreate, UserResponse
@ -94,3 +94,27 @@ def get_current_user_info(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
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=1.0.7
VERSION=1.0.8
# 版本代号 (可选)
VERSION_CODENAME="新生"

View File

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