import React, { useState, useEffect } from "react"; | |
import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | |
import Button from '@mui/material/Button'; | |
import TextField from '@mui/material/TextField'; | |
import MenuItem from '@mui/material/MenuItem'; | |
import Dialog from '@mui/material/Dialog'; | |
import DialogTitle from '@mui/material/DialogTitle'; | |
import DialogContent from '@mui/material/DialogContent'; | |
import DialogActions from '@mui/material/DialogActions'; | |
import { useNavigate } from "react-router-dom"; | |
import { API_BASE_URL } from "./config"; | |
import "./App.css"; | |
export default function UserProfile() { | |
const navigate = useNavigate(); | |
const [userInfo, setUserInfo] = useState({ | |
avatar_url: "", | |
username: "示例用户", | |
email: "user@example.com", | |
invitetimes: "", | |
school: "", | |
account_status: "", | |
gender: "", | |
}); | |
const [tempUserInfo, setTempUserInfo] = useState({ ...userInfo }); | |
const [userSeeds, setUserSeeds] = useState([]); | |
const [userFavorites, setUserFavorites] = useState([]); | |
const [userStats, setUserStats] = useState({ | |
magic: 0, | |
upload: 0, | |
download: 0, | |
ratio: 0, | |
}); | |
// 邀请相关 | |
const [inviteEmail, setInviteEmail] = useState(''); | |
const [inviteStatus, setInviteStatus] = useState(''); | |
// 兑换相关 | |
const [exchangeType, setExchangeType] = useState('uploaded'); | |
const [exchangeMagic, setExchangeMagic] = useState(''); | |
const [exchangeResult, setExchangeResult] = useState(0); | |
// 兑换比例 | |
const exchangeRate = { uploaded: 10, downloaded: 10, vip_downloads: 100 }; | |
// 用户申诉相关 | |
const [appealOpen, setAppealOpen] = useState(false); | |
const [appealTitle, setAppealTitle] = useState(''); | |
const [appealFile, setAppealFile] = useState(null); | |
// 兑换结果计算 | |
React.useEffect(() => { | |
if (!exchangeMagic || isNaN(exchangeMagic)) { | |
setExchangeResult(0); | |
return; | |
} | |
setExchangeResult(Number(exchangeMagic) / exchangeRate[exchangeType]); | |
}, [exchangeMagic, exchangeType]); | |
// 获取用户信息 | |
useEffect(() => { | |
const fetchUserInfo = async () => { | |
// 假设userid存储在localStorage或其他地方 | |
// const userid = localStorage.getItem("userid"); | |
// const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid | |
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)'); | |
const userid = match ? match[2] : null; | |
if (!userid) return; | |
try { | |
const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`); | |
if (res.ok) { | |
const data = await res.json(); | |
console.log("获取用户信息:", data); | |
setUserInfo(data); | |
setTempUserInfo(data); | |
} | |
} catch (err) { | |
console.error("获取用户信息失败", err); | |
} | |
}; | |
fetchUserInfo(); | |
}, []); | |
// 获取上传种子 | |
useEffect(() => { | |
const fetchUserSeeds = async () => { | |
// const userid = localStorage.getItem("userid"); | |
// const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid | |
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)'); | |
const userid = match ? match[2] : null; | |
if (!userid) return; | |
try { | |
const res = await fetch(`${API_BASE_URL}/api/user-seeds?userid=${userid}`); | |
if (res.ok) { | |
const data = await res.json(); | |
setUserSeeds(data); | |
} | |
} catch (err) { | |
console.error("获取种子列表失败", err); | |
} | |
}; | |
fetchUserSeeds(); | |
}, []); | |
// 收藏种子(示例数据) | |
useEffect(() => { | |
setUserFavorites([ | |
{ seedid: 'fav1', title: '收藏种子1', tags: '标签A', downloadtimes: 10 }, | |
]); | |
}, []); | |
// 获取活跃度 | |
useEffect(() => { | |
const fetchUserStats = async () => { | |
const userid = "550e8400-e29b-41d4-a716-446655440000"; | |
if (!userid) return; | |
try { | |
const res = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`); | |
if (res.ok) { | |
const data = await res.json(); | |
setUserStats(data); | |
} | |
} catch (err) { | |
console.error("获取活跃度信息失败", err); | |
} | |
}; | |
fetchUserStats(); | |
}, []); | |
const handleInputChange = (field, value) => { | |
setTempUserInfo({ ...tempUserInfo, [field]: value }); | |
}; | |
const handleSave = async () => { | |
if (tempUserInfo.gender === "男"){ | |
tempUserInfo.gender = "m"; | |
}else if (tempUserInfo.gender === "女"){ | |
tempUserInfo.gender = "f"; | |
} | |
setUserInfo({ ...tempUserInfo }); | |
// 获取userid | |
// const userid = localStorage.getItem("userid"); | |
// const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid | |
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)'); | |
const userid = match ? match[2] : null; | |
try { | |
const res = await fetch(`${API_BASE_URL}/api/change-profile`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ userid, ...tempUserInfo }), | |
}); | |
if (res.ok) { | |
alert("信息已保存!"); | |
} else { | |
alert("保存失败,请重试。"); | |
} | |
} catch (err) { | |
alert("保存失败,请检查网络连接。"); | |
console.error("保存用户信息失败", err); | |
} | |
}; | |
const handleAvatarClick = () => { | |
const avatarUrl = prompt("请输入头像的URL:"); | |
if (avatarUrl) { | |
setTempUserInfo({ ...tempUserInfo, avatar_url: avatarUrl }); | |
} | |
}; | |
// 邀请 | |
const handleInvite = () => { | |
if (!inviteEmail) return; | |
if (userInfo.invite_left <= 0) { | |
setInviteStatus("邀请次数已用完"); | |
return; | |
} | |
setInviteStatus("邀请成功!(示例,无后端)"); | |
setUserInfo((prev) => ({ | |
...prev, | |
invite_left: prev.invite_left - 1, | |
})); | |
setTempUserInfo((prev) => ({ | |
...prev, | |
invite_left: prev.invite_left - 1, | |
})); | |
setInviteEmail(''); | |
}; | |
// 兑换 | |
const handleExchange = () => { | |
const magic = Number(exchangeMagic); | |
if (!magic || isNaN(magic) || magic <= 0) return; | |
if (magic > userStats.magic) { | |
alert("魔力值不足!"); | |
return; | |
} | |
let newStats = { ...userStats }; | |
if (exchangeType === "uploaded") { | |
newStats.upload += magic / exchangeRate.uploaded; | |
} else if (exchangeType === "downloaded") { | |
newStats.download = Math.max(0, newStats.download - magic / exchangeRate.downloaded); | |
} else if (exchangeType === "vip_downloads") { | |
newStats.vip_downloads += magic / exchangeRate.vip_downloads; | |
} | |
newStats.magic -= magic; | |
setUserStats(newStats); | |
setExchangeMagic(''); | |
alert("兑换成功!(示例,无后端)"); | |
}; | |
// 删除种子 | |
const handleDeleteSeed = (seedid) => { | |
setUserSeeds(userSeeds.filter((s) => s.seedid !== seedid)); | |
}; | |
// 申诉提交逻辑 | |
const handleAppealSubmit = () => { | |
alert('申诉已提交!(示例,无后端)'); | |
setAppealOpen(false); | |
setAppealTitle(''); | |
setAppealFile(null); | |
}; | |
return ( | |
<div | |
className="container" | |
style={{ | |
minHeight: '100vh', | |
background: 'linear-gradient(135deg, #f0f4ff 0%, #e0e7ff 100%)', | |
display: 'grid', | |
gridTemplateColumns: '1.1fr 1.9fr', | |
gridTemplateRows: 'auto auto', | |
gap: '12px', | |
padding: '24px 3vw', | |
boxSizing: 'border-box' | |
}} | |
> | |
{/* 左上:用户资料 */} | |
<div style={{ | |
gridColumn: '1 / 2', | |
gridRow: '1 / 2', | |
display: 'flex', | |
flexDirection: 'column', | |
alignItems: 'center', | |
background: '#fff', | |
borderRadius: 20, | |
boxShadow: '0 6px 32px #e0e7ff', | |
padding: '32px 28px', | |
minWidth: 320, | |
minHeight: 420, | |
transition: 'box-shadow 0.2s', | |
}}> | |
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 18 }}> | |
<div onClick={handleAvatarClick} style={{ cursor: 'pointer', position: 'relative' }}> | |
<AccountCircleIcon style={{ fontSize: 96, color: '#1a237e', marginBottom: 12 }} /> | |
{tempUserInfo.avatar_url && ( | |
<img | |
src={tempUserInfo.avatar_url} | |
alt="用户头像" | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: 96, | |
height: 96, | |
borderRadius: '50%', | |
objectFit: 'cover', | |
border: '2px solid #e0e7ff', | |
boxShadow: '0 2px 8px #bfcfff' | |
}} | |
/> | |
)} | |
</div> | |
<h2 style={{ color: '#1a237e', marginBottom: 0, fontSize: 26, letterSpacing: 1 }}>用户个人资料</h2> | |
</div> | |
<div className="card" style={{ | |
padding: 32, | |
width: '100%', | |
background: '#fff', | |
borderRadius: 18, | |
boxShadow: '0 2px 12px #e0e7ff', | |
flex: 1, | |
minWidth: 0 | |
}}> | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>用户名:</b> | |
<TextField | |
variant="outlined" | |
size="small" | |
value={tempUserInfo.username} | |
onChange={(e) => handleInputChange("username", e.target.value)} | |
sx={{ flex: 1, minWidth: 0 }} | |
/> | |
</div> | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>邮箱:</b> | |
<TextField | |
variant="outlined" | |
size="small" | |
value={tempUserInfo.email} | |
InputProps={{ readOnly: true }} | |
sx={{ flex: 1, minWidth: 0, background: '#f5f5f5' }} | |
/> | |
</div> | |
{/* 邀请功能 */} | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>邀请剩余:</b> | |
<TextField | |
type="email" | |
size="small" | |
placeholder="被邀请邮箱" | |
value={inviteEmail} | |
onChange={e => setInviteEmail(e.target.value)} | |
sx={{ flex: 2, marginRight: 1, minWidth: 120 }} | |
disabled={Number(tempUserInfo.invite_left) === 0} | |
/> | |
<Button | |
variant="contained" | |
color="primary" | |
onClick={handleInvite} | |
disabled={Number(tempUserInfo.invite_left) === 0 || !inviteEmail} | |
sx={{ marginRight: 1, minWidth: 80 }} | |
>邀请</Button> | |
<span style={{ color: '#888', fontSize: 15 }}>剩余:{tempUserInfo.invite_left || "0"}</span> | |
</div> | |
{inviteStatus && <div style={{ color: '#e53935', fontSize: 14, marginBottom: 8 }}>{inviteStatus}</div>} | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>学校:</b> | |
<TextField | |
variant="outlined" | |
size="small" | |
value={tempUserInfo.school} | |
onChange={(e) => handleInputChange("school", e.target.value)} | |
sx={{ flex: 1, minWidth: 0 }} | |
/> | |
</div> | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>账号状态:</b> | |
<TextField | |
variant="outlined" | |
size="small" | |
value={tempUserInfo.account_status === 1 || tempUserInfo.account_status === "1" ? "封禁" : "正常"} | |
InputProps={{ readOnly: true }} | |
sx={{ flex: 1, minWidth: 0, background: '#f5f5f5' }} | |
/> | |
<span style={{ | |
display: 'inline-block', | |
width: 12, | |
height: 12, | |
borderRadius: '50%', | |
backgroundColor: tempUserInfo.account_status === 1 || tempUserInfo.account_status === "1" ? '#e53935' : '#43a047', | |
marginLeft: 10, | |
border: '1px solid #b2b2b2', | |
}} /> | |
</div> | |
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}> | |
<b style={{ width: 72, textAlign: 'left', marginRight: 0, fontSize: 16 }}>性别:</b> | |
<TextField | |
select | |
variant="outlined" | |
size="small" | |
value={tempUserInfo.gender} | |
onChange={e => handleInputChange("gender", e.target.value)} | |
sx={{ flex: 1, minWidth: 0 }} | |
> | |
<MenuItem value="m">男性</MenuItem> | |
<MenuItem value="f">女性</MenuItem> | |
</TextField> | |
</div> | |
<div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}> | |
<Button | |
variant="contained" | |
color="primary" | |
onClick={handleSave} | |
sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }} | |
>保存</Button> | |
<Button | |
variant="contained" | |
color="error" | |
onClick={() => setAppealOpen(true)} | |
sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }} | |
>用户申诉</Button> | |
</div> | |
</div> | |
</div> | |
{/* 左下:活跃度模块 */} | |
<div style={{ | |
gridColumn: '1 / 2', | |
gridRow: '2 / 3', | |
background: '#fff', | |
borderRadius: 20, | |
boxShadow: '0 6px 32px #e0e7ff', | |
padding: '32px 28px', | |
minWidth: 320, | |
minHeight: 320, | |
display: 'flex', | |
flexDirection: 'column', | |
justifyContent: 'center' | |
}}> | |
<h3 style={{ color: '#1a237e', fontSize: 22, marginBottom: 18, letterSpacing: 1 }}>活跃度</h3> | |
<div style={{ | |
border: '1.5px dashed #b2b2b2', | |
borderRadius: 14, | |
minHeight: 80, | |
padding: 22, | |
display: 'flex', | |
flexDirection: 'column', | |
gap: 14, | |
fontSize: 18, | |
background: '#f8faff' | |
}}> | |
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}> | |
<span>魔力值:<b style={{ color: '#1976d2' }}>{userStats.magic}</b></span> | |
<TextField | |
type="number" | |
size="small" | |
placeholder="输入兑换魔力值" | |
value={exchangeMagic} | |
onChange={e => setExchangeMagic(e.target.value)} | |
sx={{ width: 100, marginLeft: 2, marginRight: 1 }} | |
/> | |
<TextField | |
select | |
size="small" | |
value={exchangeType} | |
onChange={e => setExchangeType(e.target.value)} | |
sx={{ minWidth: 120 }} | |
> | |
<MenuItem value="uploaded">上传量(增加)</MenuItem> | |
<MenuItem value="downloaded">下载量(减少)</MenuItem> | |
<MenuItem value="vip_downloads">VIP下载次数(增加)</MenuItem> | |
</TextField> | |
<span style={{ marginLeft: 8, color: '#43a047' }}> | |
可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'GB'} | |
</span> | |
<Button | |
variant="contained" | |
color="primary" | |
onClick={handleExchange} | |
disabled={ | |
!exchangeMagic || | |
isNaN(exchangeMagic) || | |
Number(exchangeMagic) <= 0 || | |
Number(exchangeMagic) > userStats.magic | |
} | |
sx={{ | |
marginLeft: 2, | |
minWidth: 80, | |
background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic) ? '#ccc' : undefined | |
}} | |
>兑换</Button> | |
</div> | |
<div>上传量:<b style={{ color: '#43a047' }}>{userStats.upload?.toFixed(2)} GB</b></div> | |
<div>下载量:<b style={{ color: '#e53935' }}>{userStats.download?.toFixed(2)} GB</b></div> | |
<div>上传/下载值:<b style={{ color: '#ff9800' }}>{userStats.download === 0 ? "∞" : (userStats.upload / userStats.download).toFixed(2)}</b></div> | |
<div>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.vip_downloads}</b></div> | |
</div> | |
</div> | |
{/* 右上:个人上传种子列表 */} | |
<div style={{ | |
gridColumn: '2 / 3', | |
gridRow: '1 / 2', | |
background: '#fff', | |
borderRadius: 20, | |
boxShadow: '0 6px 32px #e0e7ff', | |
padding: '32px 36px', | |
minHeight: 420, | |
display: 'flex', | |
flexDirection: 'column' | |
}}> | |
<h3 style={{ color: '#1a237e', fontSize: 22, marginBottom: 18, letterSpacing: 1 }}>个人上传种子列表</h3> | |
<div style={{ | |
border: '1.5px dashed #b2b2b2', | |
borderRadius: 14, | |
minHeight: 80, | |
padding: 16, | |
background: '#f8faff' | |
}}> | |
{userSeeds.length === 0 ? ( | |
<div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无上传种子)</div> | |
) : ( | |
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}> | |
{userSeeds.map((seed, idx) => ( | |
<li | |
key={seed.seedid || idx} | |
style={{ | |
display: 'flex', | |
alignItems: 'center', | |
padding: '12px 0', | |
borderBottom: idx === userSeeds.length - 1 ? 'none' : '1px solid #e0e7ff', | |
cursor: 'pointer', | |
transition: 'background 0.15s' | |
}} | |
onClick={e => { | |
if (e.target.classList.contains('delete-btn')) return; | |
navigate(`/torrent/${seed.seed_id}`); | |
}} | |
onMouseOver={e => e.currentTarget.style.background = '#f3f6ff'} | |
onMouseOut={e => e.currentTarget.style.background = ''} | |
> | |
<span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline' }}>{seed.title}</span> | |
<span style={{ flex: 1, color: '#5c6bc0' }}>{seed.tags}</span> | |
<span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.downloadtimes}</span> | |
<Button | |
className="delete-btn" | |
variant="contained" | |
color="error" | |
size="small" | |
sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }} | |
onClick={async e => { | |
e.stopPropagation(); | |
// const userid = localStorage.getItem("userid"); | |
const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid | |
try { | |
const res = await fetch(`${API_BASE_URL}/api/delete-seed`, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ seed_id: seed.seed_id, userid }), | |
}); | |
if (res.ok) { | |
setUserSeeds(userSeeds.filter((s, i) => (s.seed_id || i) !== (seed.seed_id || idx))); | |
} else { | |
alert('删除失败,请重试'); | |
} | |
} catch (err) { | |
alert('删除失败,请检查网络'); | |
} | |
}} | |
>删除</Button> | |
</li> | |
))} | |
</ul> | |
)} | |
</div> | |
</div> | |
{/* 右下:个人收藏种子列表 */} | |
<div style={{ | |
gridColumn: '2 / 3', | |
gridRow: '2 / 3', | |
background: '#fff', | |
borderRadius: 20, | |
boxShadow: '0 6px 32px #e0e7ff', | |
padding: '32px 36px', | |
minHeight: 320, | |
display: 'flex', | |
flexDirection: 'column' | |
}}> | |
<h3 style={{ color: '#1a237e', fontSize: 22, marginBottom: 18, letterSpacing: 1 }}>个人收藏种子列表</h3> | |
<div style={{ | |
border: '1.5px dashed #b2b2b2', | |
borderRadius: 14, | |
minHeight: 80, | |
padding: 16, | |
background: '#f8faff' | |
}}> | |
{userFavorites.length === 0 ? ( | |
<div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无收藏种子)</div> | |
) : ( | |
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}> | |
{userFavorites.map((seed, idx) => ( | |
<li | |
key={seed.seedid || idx} | |
style={{ | |
display: 'flex', | |
alignItems: 'center', | |
padding: '12px 0', | |
borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff', | |
cursor: 'pointer', | |
transition: 'background 0.15s' | |
}} | |
onClick={e => { | |
navigate(`/torrent/${seed.seedid}`); | |
}} | |
onMouseOver={e => e.currentTarget.style.background = '#f3f6ff'} | |
onMouseOut={e => e.currentTarget.style.background = ''} | |
> | |
<span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline' }}>{seed.title}</span> | |
<span style={{ flex: 1, color: '#5c6bc0' }}>{seed.tags}</span> | |
<span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.downloadtimes}</span> | |
</li> | |
))} | |
</ul> | |
)} | |
</div> | |
</div> | |
{/* 申诉弹窗 */} | |
<Dialog open={appealOpen} onClose={() => setAppealOpen(false)}> | |
<DialogTitle>提交申诉</DialogTitle> | |
<DialogContent> | |
<div style={{ marginBottom: 16 }}> | |
<TextField | |
label="申诉主题" | |
fullWidth | |
value={appealTitle} | |
onChange={e => setAppealTitle(e.target.value)} | |
size="small" | |
/> | |
</div> | |
<div> | |
<input | |
type="file" | |
onChange={e => setAppealFile(e.target.files[0])} | |
style={{ marginTop: 8 }} | |
/> | |
</div> | |
</DialogContent> | |
<DialogActions> | |
<Button onClick={handleAppealSubmit} variant="contained" color="primary" disabled={!appealTitle || !appealFile}>提交</Button> | |
<Button onClick={() => setAppealOpen(false)} variant="outlined">取消</Button> | |
</DialogActions> | |
</Dialog> | |
</div> | |
); | |
} |