2026-03-18 12:49:15 +08:00
|
|
|
|
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: '',
|
2026-03-18 15:19:04 +08:00
|
|
|
|
avatar: '',
|
|
|
|
|
|
address: '',
|
2026-03-18 12:49:15 +08:00
|
|
|
|
bio: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-20 15:03:26 +08:00
|
|
|
|
// 手机验证码相关状态
|
|
|
|
|
|
const [phoneChanged, setPhoneChanged] = useState(false)
|
|
|
|
|
|
const [newPhone, setNewPhone] = useState('')
|
|
|
|
|
|
const [verifyCode, setVerifyCode] = useState('')
|
|
|
|
|
|
const [sendingCode, setSendingCode] = useState(false)
|
|
|
|
|
|
const [codeCountdown, setCodeCountdown] = useState(0)
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
const [passwordForm, setPasswordForm] = useState({
|
|
|
|
|
|
oldPassword: '',
|
|
|
|
|
|
newPassword: '',
|
|
|
|
|
|
confirmPassword: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const token = localStorage.getItem('token')
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchUserInfo()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
2026-03-20 15:03:26 +08:00
|
|
|
|
// 验证码倒计时
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (codeCountdown > 0) {
|
|
|
|
|
|
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
|
|
|
|
|
|
return () => clearTimeout(timer)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [codeCountdown])
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
const fetchUserInfo = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/users/me', {
|
|
|
|
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
|
|
|
|
})
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
const data = await res.json()
|
2026-03-18 13:10:45 +08:00
|
|
|
|
console.log('用户信息:', data)
|
2026-03-18 12:49:15 +08:00
|
|
|
|
setForm({
|
|
|
|
|
|
username: data.username || '',
|
|
|
|
|
|
email: data.email || '',
|
|
|
|
|
|
phone: data.phone || '',
|
2026-03-18 15:19:04 +08:00
|
|
|
|
avatar: data.avatar || '',
|
|
|
|
|
|
address: data.address || '',
|
2026-03-18 12:49:15 +08:00
|
|
|
|
bio: data.bio || ''
|
|
|
|
|
|
})
|
2026-03-20 15:03:26 +08:00
|
|
|
|
setNewPhone(data.phone || '')
|
2026-03-18 12:49:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-03-18 13:10:45 +08:00
|
|
|
|
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 || '',
|
2026-03-18 15:19:04 +08:00
|
|
|
|
avatar: user.avatar || '',
|
|
|
|
|
|
address: user.address || '',
|
2026-03-18 13:10:45 +08:00
|
|
|
|
bio: user.bio || ''
|
|
|
|
|
|
})
|
2026-03-20 15:03:26 +08:00
|
|
|
|
setNewPhone(user.phone || '')
|
2026-03-18 13:10:45 +08:00
|
|
|
|
} catch (e2) {
|
|
|
|
|
|
setError('获取用户信息失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 15:03:26 +08:00
|
|
|
|
// 发送手机验证码
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
const handleSubmit = async (e) => {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
setSuccess('')
|
2026-03-20 15:03:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果更换了手机号,需要验证码
|
|
|
|
|
|
if (newPhone !== form.phone) {
|
|
|
|
|
|
if (!verifyCode) {
|
|
|
|
|
|
setError('请输入手机验证码')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
setSaving(true)
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-20 15:03:26 +08:00
|
|
|
|
const updateData = {
|
|
|
|
|
|
username: form.username,
|
|
|
|
|
|
email: form.email || null,
|
|
|
|
|
|
phone: newPhone,
|
|
|
|
|
|
address: form.address,
|
|
|
|
|
|
bio: form.bio
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果更换了手机号,添加验证码
|
|
|
|
|
|
if (newPhone !== form.phone) {
|
|
|
|
|
|
updateData.verifyCode = verifyCode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
const res = await fetch('/api/users/me', {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Authorization': 'Bearer ' + token,
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
2026-03-20 15:03:26 +08:00
|
|
|
|
body: JSON.stringify(updateData)
|
2026-03-18 12:49:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
2026-03-18 13:11:30 +08:00
|
|
|
|
const data = await res.json()
|
2026-03-18 12:49:15 +08:00
|
|
|
|
setSuccess('保存成功!')
|
2026-03-20 15:03:26 +08:00
|
|
|
|
setForm({...form, phone: newPhone})
|
|
|
|
|
|
setVerifyCode('')
|
|
|
|
|
|
setPhoneChanged(false)
|
|
|
|
|
|
// 更新本地存储
|
2026-03-18 12:49:15 +08:00
|
|
|
|
const userStr = localStorage.getItem('user')
|
|
|
|
|
|
if (userStr) {
|
|
|
|
|
|
const user = JSON.parse(userStr)
|
2026-03-18 13:11:30 +08:00
|
|
|
|
user.username = data.username || user.username
|
|
|
|
|
|
user.email = data.email || user.email
|
|
|
|
|
|
user.phone = data.phone || user.phone
|
2026-03-18 12:49:15 +08:00
|
|
|
|
localStorage.setItem('user', JSON.stringify(user))
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const data = await res.json()
|
2026-03-20 15:03:26 +08:00
|
|
|
|
setError(data.error?.message || data.detail || '保存失败')
|
2026-03-18 12:49:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setError('保存失败,请重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePasswordChange = async (e) => {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
setSuccess('')
|
|
|
|
|
|
|
2026-03-18 13:38:03 +08:00
|
|
|
|
if (!passwordForm.oldPassword) {
|
|
|
|
|
|
setError('请输入当前密码')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
|
|
|
|
|
setError('两次输入的密码不一致')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 15:03:26 +08:00
|
|
|
|
if (passwordForm.newPassword.length < 8) {
|
|
|
|
|
|
setError('密码至少8位,需包含大写+小写+数字')
|
2026-03-18 12:49:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 13:38:03 +08:00
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
|
2026-03-18 12:49:15 +08:00
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
setSuccess('密码修改成功!')
|
|
|
|
|
|
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
|
|
|
|
|
} else {
|
2026-03-18 13:38:03 +08:00
|
|
|
|
setError(data.error?.message || data.detail || '密码修改失败')
|
|
|
|
|
|
}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
} 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' }}>
|
2026-03-18 15:00:39 +08:00
|
|
|
|
<div onClick={() => window.history.back()} style={{ cursor: 'pointer', fontSize: '20px', color: '#fff' }}>←</div>
|
2026-03-18 12:49:15 +08:00
|
|
|
|
<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' }}>
|
2026-03-20 15:03:26 +08:00
|
|
|
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>邮箱(选填)</label>
|
2026-03-18 12:49:15 +08:00
|
|
|
|
<input
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
value={form.email}
|
|
|
|
|
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
2026-03-20 15:03:26 +08:00
|
|
|
|
placeholder="选填"
|
2026-03-18 12:49:15 +08:00
|
|
|
|
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"
|
2026-03-20 15:03:26 +08:00
|
|
|
|
value={newPhone}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setNewPhone(e.target.value)
|
|
|
|
|
|
setPhoneChanged(true)
|
|
|
|
|
|
setVerifyCode('')
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="11位手机号"
|
|
|
|
|
|
maxLength={11}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
2026-03-18 15:19:04 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-20 15:03:26 +08:00
|
|
|
|
{/* 手机验证码 - 仅在手机号变更时显示 */}
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-18 15:19:04 +08:00
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
2026-03-20 15:03:26 +08:00
|
|
|
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>地址(选填)</label>
|
2026-03-18 15:19:04 +08:00
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={form.address}
|
|
|
|
|
|
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
2026-03-20 15:03:26 +08:00
|
|
|
|
placeholder="选填"
|
2026-03-18 15:19:04 +08:00
|
|
|
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
2026-03-20 15:03:26 +08:00
|
|
|
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>简介(选填)</label>
|
2026-03-18 12:49:15 +08:00
|
|
|
|
<textarea
|
|
|
|
|
|
value={form.bio}
|
|
|
|
|
|
onChange={(e) => setForm({ ...form, bio: e.target.value })}
|
2026-03-20 15:03:26 +08:00
|
|
|
|
placeholder="选填"
|
2026-03-18 12:49:15 +08:00
|
|
|
|
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="请输入当前密码"
|
2026-03-20 15:03:26 +08:00
|
|
|
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</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 })}
|
2026-03-20 15:03:26 +08:00
|
|
|
|
placeholder="至少8位,需包含大写+小写+数字"
|
2026-03-18 12:49:15 +08:00
|
|
|
|
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="再次输入新密码"
|
2026-03-20 15:03:26 +08:00
|
|
|
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
2026-03-18 12:49:15 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|