feat: 添加个人设置页面,支持修改个人信息和密码
This commit is contained in:
parent
f82028562d
commit
5ca8eb5ba6
|
|
@ -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": "密码修改成功"}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Version Configuration for Zodiac Collection Management System
|
||||
|
||||
# 当前版本号 (语义化版本:主版本。次版本.修订版)
|
||||
VERSION=1.0.7
|
||||
VERSION=1.0.8
|
||||
|
||||
# 版本代号 (可选)
|
||||
VERSION_CODENAME="新生"
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue