v1.2.1 阶段性稳定版本
This commit is contained in:
parent
4965bf68d3
commit
d92559a3e3
|
|
@ -1,14 +1,5 @@
|
||||||
# 甲辰藏品管理系统 - 版本配置
|
# 甲辰藏品管理系统 - 版本配置
|
||||||
# Version Configuration for Zodiac Collection Management System
|
VERSION=1.2.1
|
||||||
|
VERSION_CODENAME="阶段性稳定版本"
|
||||||
# 当前版本号 (语义化版本:主版本。次版本.修订版)
|
RELEASE_DATE=2026-03-20
|
||||||
VERSION=1.1.21
|
VERSION_NOTES="密码强度验证 - 至少8位需包含大写+小写+数字"
|
||||||
|
|
||||||
# 版本代号 (可选)
|
|
||||||
VERSION_CODENAME="新生"
|
|
||||||
|
|
||||||
# 发布日期
|
|
||||||
RELEASE_DATE=2026-03-18
|
|
||||||
|
|
||||||
# 版本说明
|
|
||||||
VERSION_NOTES="性能优化 - Nginx反向代理、图片OSS直连、自动压缩、上传限制20MB"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: jiachenlong-db-test
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: zodiac
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: python:3.11-slim
|
||||||
|
container_name: jiachenlong-backend-test
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /app
|
||||||
|
command: >
|
||||||
|
bash -c "pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic python-jose bcrypt python-multipart pillow dashscope alibabacloud-dysmsapi20170525 -q && uvicorn app.main:app --host 0.0.0.0 --port 3000"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /root/jiachenlong/backend:/app
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/zodiac
|
||||||
|
- SECRET_KEY=test-secret-key-for-sms
|
||||||
|
- ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
- OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
|
||||||
|
- OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
|
||||||
|
- OSS_BUCKET=jiachenlong-oss
|
||||||
|
- OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
||||||
|
- SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE
|
||||||
|
- SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1
|
||||||
|
- SMS_SIGN_NAME=苏州算力
|
||||||
|
- SMS_TEMPLATE_CODE=SMS_501590956
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<title>甲辰收藏 v1.1.19</title>
|
<title>甲辰收藏 v1.2.0</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export default function Admin() {
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div>
|
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div>
|
||||||
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold' }}>{user.collection_count || 0}</div>
|
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collection_count || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ export default function List() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [filter, setFilter] = useState('')
|
const [filter, setFilter] = useState('')
|
||||||
const [filterType, setFilterType] = useState('')
|
const [filterType, setFilterType] = useState('')
|
||||||
|
const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
||||||
|
const urlUserId = urlParams.get('userId') || ''
|
||||||
|
const [userIdFilter, setUserIdFilter] = useState(urlUserId)
|
||||||
const [sortField, setSortField] = useState('createdAt')
|
const [sortField, setSortField] = useState('createdAt')
|
||||||
const [sortOrder, setSortOrder] = useState('desc')
|
const [sortOrder, setSortOrder] = useState('desc')
|
||||||
const [viewMode, setViewMode] = useState('list')
|
const [viewMode, setViewMode] = useState('list')
|
||||||
|
|
@ -80,6 +83,11 @@ export default function List() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用筛选条件
|
// 应用筛选条件
|
||||||
|
// 用户ID筛选(从URL获取)
|
||||||
|
if (userIdFilter) {
|
||||||
|
list = list.filter(item => item.userId === userIdFilter || item.userId === userIdFilter.replace(/-/g, ''))
|
||||||
|
}
|
||||||
|
|
||||||
if (filterType && filter) {
|
if (filterType && filter) {
|
||||||
const fieldMap = {
|
const fieldMap = {
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,83 @@ export default function Login() {
|
||||||
|
|
||||||
// 注册相关状态
|
// 注册相关状态
|
||||||
const [showRegister, setShowRegister] = useState(false)
|
const [showRegister, setShowRegister] = useState(false)
|
||||||
const [registerData, setRegisterData] = useState({ username: '', password: '', confirmPassword: '', email: '' })
|
const [registerData, setRegisterData] = useState({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
verifyCode: ''
|
||||||
|
})
|
||||||
const [registerLoading, setRegisterLoading] = useState(false)
|
const [registerLoading, setRegisterLoading] = useState(false)
|
||||||
const [registerError, setRegisterError] = useState('')
|
const [registerError, setRegisterError] = useState('')
|
||||||
|
|
||||||
// 生成验证码
|
// 短信获取相关
|
||||||
|
const [sendingCode, setSendingCode] = useState(false)
|
||||||
|
const [codeCountdown, setCodeCountdown] = useState(0)
|
||||||
|
const [codeSent, setCodeSent] = useState(false)
|
||||||
|
|
||||||
|
// 生成获取
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateCaptcha()
|
generateCaptcha()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 获取倒计时
|
||||||
|
useEffect(() => {
|
||||||
|
if (codeCountdown > 0) {
|
||||||
|
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [codeCountdown])
|
||||||
|
|
||||||
const generateCaptcha = () => {
|
const generateCaptcha = () => {
|
||||||
const num1 = Math.floor(Math.random() * 10)
|
const num1 = Math.floor(Math.random() * 10)
|
||||||
const num2 = Math.floor(Math.random() * 10)
|
const num2 = Math.floor(Math.random() * 10)
|
||||||
setCaptcha({ num1, num2, answer: '' })
|
setCaptcha({ num1, num2, answer: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送短信获取
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (!registerData.phone) {
|
||||||
|
setRegisterError('请先输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号格式
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/
|
||||||
|
if (!phoneRegex.test(registerData.phone)) {
|
||||||
|
setRegisterError('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSendingCode(true)
|
||||||
|
setRegisterError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/send-verification-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ phone: registerData.phone })
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setCodeSent(true)
|
||||||
|
setCodeCountdown(60)
|
||||||
|
setRegisterError('')
|
||||||
|
} else {
|
||||||
|
setRegisterError(data.message || '发送失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setRegisterError('发送失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
setSendingCode(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理注册
|
// 处理注册
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
// 验证输入
|
// 验证输入
|
||||||
|
|
@ -35,13 +97,13 @@ export default function Login() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registerData.username.length < 3) {
|
if (registerData.username.length < 2) {
|
||||||
setRegisterError('用户名至少 3 个字符')
|
setRegisterError('用户名至少 2 个字符')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registerData.password.length < 6) {
|
if (registerData.password.length < 8 || !/[A-Z]/.test(registerData.password) || !/[a-z]/.test(registerData.password) || !/[0-9]/.test(registerData.password)) {
|
||||||
setRegisterError('密码至少 6 个字符')
|
setRegisterError('密码至少8位,需包含大写、小写、数字')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,13 +112,33 @@ export default function Login() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证手机号
|
||||||
|
if (!registerData.phone) {
|
||||||
|
setRegisterError('手机号为必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/
|
||||||
|
if (!phoneRegex.test(registerData.phone)) {
|
||||||
|
setRegisterError('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证短信获取
|
||||||
|
if (!registerData.verifyCode) {
|
||||||
|
setRegisterError('请输入短信获取')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setRegisterLoading(true)
|
setRegisterLoading(true)
|
||||||
setRegisterError('')
|
setRegisterError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
username: registerData.username,
|
username: registerData.username,
|
||||||
password: registerData.password
|
password: registerData.password,
|
||||||
|
phone: registerData.phone,
|
||||||
|
verifyCode: registerData.verifyCode
|
||||||
}
|
}
|
||||||
if (registerData.email) {
|
if (registerData.email) {
|
||||||
payload.email = registerData.email
|
payload.email = registerData.email
|
||||||
|
|
@ -73,12 +155,14 @@ export default function Login() {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(data.error?.message || '注册失败')
|
throw new Error(data.detail || data.error?.message || '注册失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
alert('注册成功!请登录')
|
alert('注册成功!请登录')
|
||||||
setShowRegister(false)
|
setShowRegister(false)
|
||||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '' })
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
|
||||||
|
setCodeSent(false)
|
||||||
|
setCodeCountdown(0)
|
||||||
generateCaptcha()
|
generateCaptcha()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('注册错误:', err)
|
console.error('注册错误:', err)
|
||||||
|
|
@ -95,17 +179,13 @@ export default function Login() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length < 3) {
|
if (username.length < 2) {
|
||||||
setError(ErrorCodes.E00021.message)
|
setError(ErrorCodes.E00021.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
setError(ErrorCodes.E00022.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证验证码
|
// 验证获取
|
||||||
const expectedAnswer = captcha.num1 + captcha.num2
|
const expectedAnswer = captcha.num1 + captcha.num2
|
||||||
if (!captcha.answer || Number(captcha.answer) !== expectedAnswer) {
|
if (!captcha.answer || Number(captcha.answer) !== expectedAnswer) {
|
||||||
setError(ErrorCodes.E00012.message)
|
setError(ErrorCodes.E00012.message)
|
||||||
|
|
@ -241,11 +321,11 @@ export default function Login() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 验证码 */}
|
{/* 获取 */}
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div style={{ marginBottom: '24px' }}>
|
||||||
<div style={{ display: 'flex', gap: '12px' }}>
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
<div onClick={generateCaptcha} style={{
|
<div onClick={generateCaptcha} style={{
|
||||||
flex: 1,
|
flex: 1, minWidth: "90px", width: "auto",
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid rgba(255,255,255,0.2)',
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
|
@ -355,7 +435,9 @@ export default function Login() {
|
||||||
background: 'rgba(30, 41, 59, 0.95)',
|
background: 'rgba(30, 41, 59, 0.95)',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
border: '1px solid rgba(255,255,255,0.1)'
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto'
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '600', marginBottom: '20px', textAlign: 'center' }}>
|
<h2 style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '600', marginBottom: '20px', textAlign: 'center' }}>
|
||||||
注册新账号 🎉
|
注册新账号 🎉
|
||||||
|
|
@ -381,7 +463,7 @@ export default function Login() {
|
||||||
type="text"
|
type="text"
|
||||||
value={registerData.username}
|
value={registerData.username}
|
||||||
onChange={(e) => setRegisterData(prev => ({ ...prev, username: e.target.value }))}
|
onChange={(e) => setRegisterData(prev => ({ ...prev, username: e.target.value }))}
|
||||||
placeholder="用户名(至少 3 个字符)"
|
placeholder="用户名(至少 2 个字符)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
|
|
@ -395,6 +477,68 @@ export default function Login() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 手机号 */}
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={registerData.phone}
|
||||||
|
onChange={(e) => setRegisterData(prev => ({ ...prev, phone: e.target.value }))}
|
||||||
|
placeholder="手机号(11位)"
|
||||||
|
maxLength={11}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '16px',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 短信获取 */}
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={registerData.verifyCode}
|
||||||
|
onChange={(e) => setRegisterData(prev => ({ ...prev, verifyCode: e.target.value }))}
|
||||||
|
placeholder="短信验证码"
|
||||||
|
maxLength={6}
|
||||||
|
style={{
|
||||||
|
flex: 1, minWidth: "90px", width: "auto",
|
||||||
|
padding: '14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '16px',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSendCode}
|
||||||
|
disabled={sendingCode || codeCountdown > 0}
|
||||||
|
style={{
|
||||||
|
minWidth: "90px", width: "auto",
|
||||||
|
padding: '14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
background: codeCountdown > 0 ? 'rgba(148, 163, 184, 0.3)' : 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: codeCountdown > 0 ? 'not-allowed' : 'pointer',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{codeCountdown > 0 ? `${codeCountdown}秒` : sendingCode ? '发送中...' : '获取验证码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 邮箱(可选) */}
|
{/* 邮箱(可选) */}
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<input
|
<input
|
||||||
|
|
@ -421,7 +565,7 @@ export default function Login() {
|
||||||
type="password"
|
type="password"
|
||||||
value={registerData.password}
|
value={registerData.password}
|
||||||
onChange={(e) => setRegisterData(prev => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setRegisterData(prev => ({ ...prev, password: e.target.value }))}
|
||||||
placeholder="密码(至少 6 个字符)"
|
placeholder="密码(至少8位,需大写+小写+数字)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
|
|
@ -460,11 +604,13 @@ export default function Login() {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRegister(false)
|
setShowRegister(false)
|
||||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '' })
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
|
||||||
setRegisterError('')
|
setRegisterError('')
|
||||||
|
setCodeSent(false)
|
||||||
|
setCodeCountdown(0)
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1, minWidth: "90px", width: "auto",
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|
@ -481,7 +627,7 @@ export default function Login() {
|
||||||
onClick={handleRegister}
|
onClick={handleRegister}
|
||||||
disabled={registerLoading}
|
disabled={registerLoading}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1, minWidth: "90px", width: "auto",
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ export default function Settings() {
|
||||||
bio: ''
|
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({
|
const [passwordForm, setPasswordForm] = useState({
|
||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
|
|
@ -29,6 +36,14 @@ export default function Settings() {
|
||||||
fetchUserInfo()
|
fetchUserInfo()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 验证码倒计时
|
||||||
|
useEffect(() => {
|
||||||
|
if (codeCountdown > 0) {
|
||||||
|
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [codeCountdown])
|
||||||
|
|
||||||
const fetchUserInfo = async () => {
|
const fetchUserInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/users/me', {
|
const res = await fetch('/api/users/me', {
|
||||||
|
|
@ -45,12 +60,10 @@ export default function Settings() {
|
||||||
address: data.address || '',
|
address: data.address || '',
|
||||||
bio: data.bio || ''
|
bio: data.bio || ''
|
||||||
})
|
})
|
||||||
} else {
|
setNewPhone(data.phone || '')
|
||||||
console.error('获取用户信息失败:', res.status)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取用户信息异常:', e)
|
console.error('获取用户信息异常:', e)
|
||||||
// 尝试从本地存储获取
|
|
||||||
const userStr = localStorage.getItem('user')
|
const userStr = localStorage.getItem('user')
|
||||||
if (userStr) {
|
if (userStr) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -63,6 +76,7 @@ export default function Settings() {
|
||||||
address: user.address || '',
|
address: user.address || '',
|
||||||
bio: user.bio || ''
|
bio: user.bio || ''
|
||||||
})
|
})
|
||||||
|
setNewPhone(user.phone || '')
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
setError('获取用户信息失败')
|
setError('获取用户信息失败')
|
||||||
}
|
}
|
||||||
|
|
@ -72,26 +86,92 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送手机验证码
|
||||||
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
setSuccess('')
|
setSuccess('')
|
||||||
|
|
||||||
|
// 如果更换了手机号,需要验证码
|
||||||
|
if (newPhone !== form.phone) {
|
||||||
|
if (!verifyCode) {
|
||||||
|
setError('请输入手机验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
try {
|
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', {
|
const res = await fetch('/api/users/me', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + token,
|
'Authorization': 'Bearer ' + token,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(form)
|
body: JSON.stringify(updateData)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setSuccess('保存成功!')
|
setSuccess('保存成功!')
|
||||||
// 更新本地存储的用户信息
|
setForm({...form, phone: newPhone})
|
||||||
|
setVerifyCode('')
|
||||||
|
setPhoneChanged(false)
|
||||||
|
// 更新本地存储
|
||||||
const userStr = localStorage.getItem('user')
|
const userStr = localStorage.getItem('user')
|
||||||
if (userStr) {
|
if (userStr) {
|
||||||
const user = JSON.parse(userStr)
|
const user = JSON.parse(userStr)
|
||||||
|
|
@ -102,7 +182,7 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setError(data.error?.message || '保存失败')
|
setError(data.error?.message || data.detail || '保存失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('保存失败,请重试')
|
setError('保存失败,请重试')
|
||||||
|
|
@ -126,16 +206,14 @@ export default function Settings() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordForm.newPassword.length < 6) {
|
if (passwordForm.newPassword.length < 8) {
|
||||||
setError('密码长度至少6位')
|
setError('密码至少8位,需包含大写+小写+数字')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
console.log('开始修改密码...')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('发送请求到 /api/auth/change-password')
|
|
||||||
const res = await fetch('/api/auth/change-password', {
|
const res = await fetch('/api/auth/change-password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -148,9 +226,7 @@ export default function Settings() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('响应状态:', res.status)
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
console.log('响应数据:', data)
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setSuccess('密码修改成功!')
|
setSuccess('密码修改成功!')
|
||||||
|
|
@ -200,11 +276,12 @@ export default function Settings() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>邮箱</label>
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>邮箱(选填)</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||||
|
placeholder="选填"
|
||||||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,27 +290,69 @@ export default function Settings() {
|
||||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>手机号</label>
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>手机号</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={form.phone}
|
value={newPhone}
|
||||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
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' }}
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>地址</label>
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>地址(选填)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.address}
|
value={form.address}
|
||||||
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
||||||
|
placeholder="选填"
|
||||||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>简介</label>
|
<label style={{ display: 'block', color: '#94a3b8', fontSize: '14px', marginBottom: '8px' }}>简介(选填)</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={form.bio}
|
value={form.bio}
|
||||||
onChange={(e) => setForm({ ...form, bio: e.target.value })}
|
onChange={(e) => setForm({ ...form, bio: e.target.value })}
|
||||||
|
placeholder="选填"
|
||||||
rows={3}
|
rows={3}
|
||||||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff', resize: 'none' }}
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff', resize: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -260,8 +379,8 @@ export default function Settings() {
|
||||||
type="password"
|
type="password"
|
||||||
value={passwordForm.oldPassword}
|
value={passwordForm.oldPassword}
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
|
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
|
||||||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
|
||||||
placeholder="请输入当前密码"
|
placeholder="请输入当前密码"
|
||||||
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -271,8 +390,8 @@ export default function Settings() {
|
||||||
type="password"
|
type="password"
|
||||||
value={passwordForm.newPassword}
|
value={passwordForm.newPassword}
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
|
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' }}
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
placeholder="至少6位"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -282,8 +401,8 @@ export default function Settings() {
|
||||||
type="password"
|
type="password"
|
||||||
value={passwordForm.confirmPassword}
|
value={passwordForm.confirmPassword}
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
||||||
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
|
||||||
placeholder="再次输入新密码"
|
placeholder="再次输入新密码"
|
||||||
|
style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid #334155', background: '#0f172a', color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue