blob: f6ef9f406744d413a26d5cf20fd4cf9f7f47036c [file] [log] [blame]
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>
);
}