435 lines
15 KiB
JavaScript
435 lines
15 KiB
JavaScript
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: '',
|
||
avatar: '',
|
||
address: '',
|
||
bio: ''
|
||
})
|
||
|
||
// 手机验证码相关状态
|
||
const [phoneChanged, setPhoneChanged] = useState(false)
|
||
const [newPhone, setNewPhone] = useState('')
|
||
const [verifyCode, setVerifyCode] = useState('')
|
||
const [sendingCode, setSendingCode] = useState(false)
|
||
const [codeCountdown, setCodeCountdown] = useState(0)
|
||
|
||
const [passwordForm, setPasswordForm] = useState({
|
||
oldPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
|
||
const navigate = useNavigate()
|
||
const token = localStorage.getItem('token')
|
||
|
||
useEffect(() => {
|
||
fetchUserInfo()
|
||
}, [])
|
||
|
||
// 验证码倒计时
|
||
useEffect(() => {
|
||
if (codeCountdown > 0) {
|
||
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [codeCountdown])
|
||
|
||
const fetchUserInfo = async () => {
|
||
try {
|
||
const res = await fetch('/api/users/me', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
})
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
console.log('用户信息:', data)
|
||
setForm({
|
||
username: data.username || '',
|
||
email: data.email || '',
|
||
phone: data.phone || '',
|
||
avatar: data.avatar || '',
|
||
address: data.address || '',
|
||
bio: data.bio || ''
|
||
})
|
||
setNewPhone(data.phone || '')
|
||
}
|
||
} catch (e) {
|
||
console.error('获取用户信息异常:', e)
|
||
const userStr = localStorage.getItem('user')
|
||
if (userStr) {
|
||
try {
|
||
const user = JSON.parse(userStr)
|
||
setForm({
|
||
username: user.username || '',
|
||
email: user.email || '',
|
||
phone: user.phone || '',
|
||
avatar: user.avatar || '',
|
||
address: user.address || '',
|
||
bio: user.bio || ''
|
||
})
|
||
setNewPhone(user.phone || '')
|
||
} catch (e2) {
|
||
setError('获取用户信息失败')
|
||
}
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 发送手机验证码
|
||
const handleSendCode = async () => {
|
||
if (!newPhone) {
|
||
setError('请输入手机号')
|
||
return
|
||
}
|
||
|
||
const phoneRegex = /^1[3-9]\d{9}$/
|
||
if (!phoneRegex.test(newPhone)) {
|
||
setError('请输入正确的手机号')
|
||
return
|
||
}
|
||
|
||
setSendingCode(true)
|
||
setError('')
|
||
|
||
try {
|
||
const res = await fetch('/api/auth/send-verification-code', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ phone: newPhone })
|
||
})
|
||
|
||
const data = await res.json()
|
||
|
||
if (data.success) {
|
||
setPhoneChanged(true)
|
||
setCodeCountdown(60)
|
||
setSuccess('验证码已发送到 ' + newPhone.substring(0,3) + '****' + newPhone.substring(7))
|
||
} else {
|
||
setError(data.message || '发送失败')
|
||
}
|
||
} catch (err) {
|
||
setError('发送失败,请稍后重试')
|
||
} finally {
|
||
setSendingCode(false)
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault()
|
||
setError('')
|
||
setSuccess('')
|
||
|
||
// 如果更换了手机号,需要验证码
|
||
if (newPhone !== form.phone) {
|
||
if (!verifyCode) {
|
||
setError('请输入手机验证码')
|
||
return
|
||
}
|
||
}
|
||
|
||
setSaving(true)
|
||
|
||
try {
|
||
const updateData = {
|
||
username: form.username,
|
||
email: form.email || null,
|
||
phone: newPhone,
|
||
address: form.address,
|
||
bio: form.bio
|
||
}
|
||
|
||
// 如果更换了手机号,添加验证码
|
||
if (newPhone !== form.phone) {
|
||
updateData.verifyCode = verifyCode
|
||
}
|
||
|
||
const res = await fetch('/api/users/me', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(updateData)
|
||
})
|
||
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
setSuccess('保存成功!')
|
||
setForm({...form, phone: newPhone})
|
||
setVerifyCode('')
|
||
setPhoneChanged(false)
|
||
// 更新本地存储
|
||
const userStr = localStorage.getItem('user')
|
||
if (userStr) {
|
||
const user = JSON.parse(userStr)
|
||
user.username = data.username || user.username
|
||
user.email = data.email || user.email
|
||
user.phone = data.phone || user.phone
|
||
localStorage.setItem('user', JSON.stringify(user))
|
||
}
|
||
} else {
|
||
const data = await res.json()
|
||
setError(data.error?.message || data.detail || '保存失败')
|
||
}
|
||
} catch (e) {
|
||
setError('保存失败,请重试')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const handlePasswordChange = async (e) => {
|
||
e.preventDefault()
|
||
setError('')
|
||
setSuccess('')
|
||
|
||
if (!passwordForm.oldPassword) {
|
||
setError('请输入当前密码')
|
||
return
|
||
}
|
||
|
||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||
setError('两次输入的密码不一致')
|
||
return
|
||
}
|
||
|
||
if (passwordForm.newPassword.length < 8) {
|
||
setError('密码至少8位,需包含大写+小写+数字')
|
||
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
|
||
})
|
||
})
|
||
|
||
const data = await res.json()
|
||
|
||
if (res.ok) {
|
||
setSuccess('密码修改成功!')
|
||
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
||
} else {
|
||
setError(data.error?.message || data.detail || '密码修改失败')
|
||
}
|
||
} 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={() => window.history.back()} style={{ cursor: 'pointer', fontSize: '20px', color: '#fff' }}>←</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 })}
|
||
placeholder="选填"
|
||
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={newPhone}
|
||
onChange={(e) => {
|
||
setNewPhone(e.target.value)
|
||
setPhoneChanged(true)
|
||
setVerifyCode('')
|
||
}}
|
||
placeholder="11位手机号"
|
||
maxLength={11}
|
||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||
/>
|
||
</div>
|
||
|
||
{/* 手机验证码 - 仅在手机号变更时显示 */}
|
||
{phoneChanged && (
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>手机验证码</label>
|
||
<div style={{ display: 'flex', gap: '12px' }}>
|
||
<input
|
||
type="text"
|
||
value={verifyCode}
|
||
onChange={(e) => setVerifyCode(e.target.value)}
|
||
placeholder="6位验证码"
|
||
maxLength={6}
|
||
style={{ flex: 1, padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={handleSendCode}
|
||
disabled={sendingCode || codeCountdown > 0}
|
||
style={{
|
||
width: '120px',
|
||
padding: '12px',
|
||
borderRadius: '8px',
|
||
border: 'none',
|
||
background: codeCountdown > 0 ? '#4b5563' : '#22c55e',
|
||
color: '#fff',
|
||
fontSize: '14px',
|
||
cursor: codeCountdown > 0 ? 'not-allowed' : 'pointer'
|
||
}}
|
||
>
|
||
{codeCountdown > 0 ? codeCountdown + '秒' : sendingCode ? '发送中...' : '获取验证码'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>地址(选填)</label>
|
||
<input
|
||
type="text"
|
||
value={form.address}
|
||
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
||
placeholder="选填"
|
||
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 })}
|
||
placeholder="选填"
|
||
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 })}
|
||
placeholder="请输入当前密码"
|
||
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="password"
|
||
value={passwordForm.newPassword}
|
||
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
|
||
placeholder="至少8位,需包含大写+小写+数字"
|
||
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="password"
|
||
value={passwordForm.confirmPassword}
|
||
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
||
placeholder="再次输入新密码"
|
||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||
/>
|
||
</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>
|
||
)
|
||
}
|