jiachenlong/frontend/src/pages/Settings.jsx

441 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Settings - 设置页面
* Version: 0.0.1x
* 更新:
*/
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>
)
}