新增求种和个人中心接口
Change-Id: Ibf3eef5b91a45a0ccf7b99d08afa29960884a8cf
diff --git a/front/src/UserProfile.js b/front/src/UserProfile.js
index f6ef9f4..eed84da 100644
--- a/front/src/UserProfile.js
+++ b/front/src/UserProfile.js
@@ -28,7 +28,7 @@
const [userStats, setUserStats] = useState({
magic: 0,
upload: 0,
- download: 0,
+ viptime: 0,
ratio: 0,
});
@@ -42,13 +42,18 @@
const [exchangeResult, setExchangeResult] = useState(0);
// 兑换比例
- const exchangeRate = { uploaded: 10, downloaded: 10, vip_downloads: 100 };
+ const exchangeRate = { uploaded: 0.1, downloaded: 0.1, vip_downloads: 100 };
// 用户申诉相关
const [appealOpen, setAppealOpen] = useState(false);
const [appealTitle, setAppealTitle] = useState('');
const [appealFile, setAppealFile] = useState(null);
+ // 账号迁移相关
+ const [migrationOpen, setMigrationOpen] = useState(false);
+ const [migrationEmail, setMigrationEmail] = useState('');
+ const [migrationPassword, setMigrationPassword] = useState('');
+ const [migrationStatus, setMigrationStatus] = useState('');
// 兑换结果计算
React.useEffect(() => {
if (!exchangeMagic || isNaN(exchangeMagic)) {
@@ -71,7 +76,7 @@
const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`);
if (res.ok) {
const data = await res.json();
- console.log("获取用户信息:", data);
+ // console.log("获取用户信息:", data);
setUserInfo(data);
setTempUserInfo(data);
}
@@ -102,18 +107,30 @@
};
fetchUserSeeds();
}, []);
-
- // 收藏种子(示例数据)
+ // 获取收藏种子
useEffect(() => {
- setUserFavorites([
- { seedid: 'fav1', title: '收藏种子1', tags: '标签A', downloadtimes: 10 },
- ]);
+ const fetchUserFavorites = async () => {
+ 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-favorites?userid=${userid}`);
+ if (res.ok) {
+ const data = await res.json();
+ // console.log("获取收藏种子列表:", data);
+ setUserFavorites(data);
+ }
+ } catch (err) {
+ console.error("获取收藏种子列表失败", err);
+ }
+ };
+ fetchUserFavorites();
}, []);
-
// 获取活跃度
useEffect(() => {
const fetchUserStats = async () => {
- const userid = "550e8400-e29b-41d4-a716-446655440000";
+ 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-stats?userid=${userid}`);
@@ -171,57 +188,205 @@
};
// 邀请
- const handleInvite = () => {
- if (!inviteEmail) return;
+ const handleInvite = async () => {
+ if (!inviteEmail) {
+ setInviteStatus("请输入邀请邮箱");
+ return;
+ }
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ setInviteStatus("未获取到用户ID");
+ 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 = () => {
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/invite`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid, invite_email: inviteEmail }),
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setInviteStatus("邀请成功");
+ // 更新剩余次数
+ const left = data.invite_left !== undefined ? data.invite_left : userInfo.invite_left - 1;
+ setUserInfo(prev => ({ ...prev, invite_left: left }));
+ setTempUserInfo(prev => ({ ...prev, invite_left: left }));
+ setInviteEmail('');
+ } else {
+ const errorText = await res.text();
+ setInviteStatus("邀请失败:" + errorText);
+ }
+ } catch (err) {
+ console.error("邀请失败", err);
+ setInviteStatus("邀请失败,请检查网络");
+ }
+ }; // 兑换
+ const handleExchange = async () => {
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;
+
+ // 检查兑换结果是否为整数
+ const calculatedExchangeResult = magic / exchangeRate[exchangeType];
+ if (!Number.isInteger(calculatedExchangeResult)) {
+ alert("兑换结果必须为整数,请调整魔力值!");
+ return;
}
- newStats.magic -= magic;
- setUserStats(newStats);
- setExchangeMagic('');
- alert("兑换成功!(示例,无后端)");
- };
+
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert("未获取到用户ID");
+ return;
+ }
+ console.log("兑换请求参数:", { userid, magic, exchangeType, exchangeResult: calculatedExchangeResult });
+ try {
+ // 发送兑换请求到后端
+ const res = await fetch(`${API_BASE_URL}/api/exchange`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ userid,
+ magic,
+ exchangeType,
+ exchangeResult: calculatedExchangeResult
+ }),
+ });
+ // console.log("兑换请求结果:", res);
+ if (res.ok) {
+ // 兑换成功后重新获取用户数据
+ const statsRes = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);
+ if (statsRes.ok) {
+ const updatedStats = await statsRes.json();
+ setUserStats(updatedStats);
+ }
+ setExchangeMagic('');
+ alert("兑换成功!");
+ } else {
+ const errorText = await res.text();
+ alert("兑换失败:" + errorText);
+ }
+ } catch (err) {
+ console.error("兑换失败", err);
+ alert("兑换失败,请检查网络");
+ }
+ };
// 删除种子
const handleDeleteSeed = (seedid) => {
setUserSeeds(userSeeds.filter((s) => s.seedid !== seedid));
};
+ // 取消收藏
+ const handleRemoveFavorite = async (seedid) => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/remove-favorite`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid, seedid }),
+ }); if (res.ok) {
+ setUserFavorites(userFavorites.filter((s) => (s.seedid || s.seed_id) !== seedid));
+ alert('已取消收藏');
+ } else {
+ alert('取消收藏失败,请重试');
+ }
+ } catch (err) {
+ console.error('取消收藏失败', err);
+ alert('取消收藏失败,请检查网络');
+ }
+ };
+
// 申诉提交逻辑
- const handleAppealSubmit = () => {
- alert('申诉已提交!(示例,无后端)');
- setAppealOpen(false);
- setAppealTitle('');
- setAppealFile(null);
+ const handleAppealSubmit = async () => {
+ if (!appealTitle || !appealFile) return;
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
+ // 构建表单数据
+ const formData = new FormData();
+ formData.append('userid', userid);
+ formData.append('content', appealTitle);
+ formData.append('file', appealFile);
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/submit-appeal`, {
+ method: 'POST',
+ body: formData,
+ });
+ if (res.ok) {
+ alert('申诉已提交');
+ setAppealOpen(false);
+ setAppealTitle('');
+ setAppealFile(null);
+ } else {
+ const errorText = await res.text();
+ alert('申诉失败:' + errorText);
+ }
+ } catch (err) {
+ console.error('申诉失败', err);
+ alert('申诉失败,请检查网络');
+ }
+ };
+ // 账号迁移提交逻辑
+ const handleMigrationSubmit = async () => {
+ if (!appealFile) {
+ setMigrationStatus('请选择PDF文件');
+ return;
+ }
+
+ // 获取当前用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const currentUserId = match ? match[2] : null;
+ if (!currentUserId) {
+ setMigrationStatus('未获取到当前用户ID');
+ return;
+ }
+
+ try {
+ // 构建表单数据
+ const formData = new FormData();
+ formData.append('userid', currentUserId);
+ formData.append('file', appealFile);
+
+ const res = await fetch(`${API_BASE_URL}/api/migrate-account`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (res.ok) {
+ setMigrationStatus('账号迁移申请已提交,请等待管理员审核');
+ setTimeout(() => {
+ setMigrationOpen(false);
+ setAppealFile(null);
+ setMigrationStatus('');
+ }, 2000);
+ } else {
+ const errorText = await res.text();
+ setMigrationStatus('迁移失败:' + errorText);
+ }
+ } catch (err) {
+ console.error('账号迁移失败', err);
+ setMigrationStatus('迁移失败,请检查网络');
+ }
};
return (
@@ -369,20 +534,25 @@
<MenuItem value="m">男性</MenuItem>
<MenuItem value="f">女性</MenuItem>
</TextField>
- </div>
- <div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}>
+ </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' }}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
>保存</Button>
<Button
variant="contained"
color="error"
onClick={() => setAppealOpen(true)}
- sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
>用户申诉</Button>
+ <Button
+ variant="contained"
+ color="warning"
+ onClick={() => setMigrationOpen(true)}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
+ >账号迁移</Button>
</div>
</div>
</div>
@@ -432,11 +602,14 @@
<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
+ </TextField> <span style={{ marginLeft: 8, color: '#43a047' }}>
+ 可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'MB'}
+ {!Number.isInteger(exchangeResult) && exchangeResult > 0 && (
+ <span style={{ color: '#e53935', fontSize: '12px', marginLeft: 8 }}>
+ (结果必须为整数)
+ </span>
+ )}
+ </span><Button
variant="contained"
color="primary"
onClick={handleExchange}
@@ -444,19 +617,19 @@
!exchangeMagic ||
isNaN(exchangeMagic) ||
Number(exchangeMagic) <= 0 ||
- Number(exchangeMagic) > userStats.magic
+ Number(exchangeMagic) > userStats.magic ||
+ !Number.isInteger(exchangeResult)
}
sx={{
marginLeft: 2,
minWidth: 80,
- background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic) ? '#ccc' : undefined
+ background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic || !Number.isInteger(exchangeResult)) ? '#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> <div>上传量:<b style={{ color: '#43a047' }}>{(userStats.upload / 1000000)?.toFixed(2)} MB</b></div>
+ <div>下载量:<b style={{ color: '#e53935' }}>{(userStats.download / 1000000)?.toFixed(2)} MB</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>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.viptime}</b></div>
</div>
</div>
{/* 右上:个人上传种子列表 */}
@@ -509,11 +682,14 @@
variant="contained"
color="error"
size="small"
- sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }}
- onClick={async e => {
+ 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
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
try {
const res = await fetch(`${API_BASE_URL}/api/delete-seed`, {
method: 'POST',
@@ -558,8 +734,7 @@
}}>
{userFavorites.length === 0 ? (
<div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无收藏种子)</div>
- ) : (
- <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
+ ) : ( <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{userFavorites.map((seed, idx) => (
<li
key={seed.seedid || idx}
@@ -570,16 +745,25 @@
borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff',
cursor: 'pointer',
transition: 'background 0.15s'
- }}
- onClick={e => {
- navigate(`/torrent/${seed.seedid}`);
+ }} onClick={e => { if (e.target.classList.contains('remove-favorite-btn')) return;
+ navigate(`/torrent/${seed.seedid || 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>
+ <span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline', cursor: 'pointer' }}>{seed.seed.title}</span>
+ <span style={{ flex: 1, color: '#5c6bc0' }}>{seed.seed.tags}</span>
+ <span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.seed.downloadtimes}</span>
+ <Button
+ className="remove-favorite-btn"
+ variant="contained"
+ color="warning"
+ size="small"
+ sx={{ marginLeft: 2, borderRadius: 1, minWidth: 80 }} onClick={e => {
+ e.stopPropagation();
+ handleRemoveFavorite(seed.seedid || seed.seed_id);
+ }}
+ >取消收藏</Button>
</li>
))}
</ul>
@@ -598,19 +782,66 @@
onChange={e => setAppealTitle(e.target.value)}
size="small"
/>
- </div>
- <div>
+ </div> <div>
<input
type="file"
- onChange={e => setAppealFile(e.target.files[0])}
+ accept=".pdf"
+ onChange={e => {
+ const file = e.target.files[0];
+ if (file && file.type !== 'application/pdf') {
+ alert('请选择PDF文件');
+ e.target.value = '';
+ setAppealFile(null);
+ } else {
+ setAppealFile(file);
+ }
+ }}
style={{ marginTop: 8 }}
/>
+ <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>
+ 请选择PDF文件(最大100MB)
+ </div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={handleAppealSubmit} variant="contained" color="primary" disabled={!appealTitle || !appealFile}>提交</Button>
<Button onClick={() => setAppealOpen(false)} variant="outlined">取消</Button>
</DialogActions>
+ </Dialog> {/* 账号迁移弹窗 */}
+ <Dialog open={migrationOpen} onClose={() => setMigrationOpen(false)}>
+ <DialogTitle>账号迁移</DialogTitle>
+ <DialogContent>
+ <div style={{ marginBottom: 16 }}>
+ </div> <div>
+ <input
+ type="file"
+ accept=".pdf"
+ onChange={e => {
+ const file = e.target.files[0];
+ if (file && file.type !== 'application/pdf') {
+ alert('请选择PDF文件');
+ e.target.value = '';
+ setAppealFile(null);
+ } else {
+ setAppealFile(file);
+ }
+ }}
+ style={{ marginTop: 8 }}
+ />
+ <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>
+ 请选择PDF文件(最大10MB)
+ </div>
+ </div>
+ {migrationStatus && (
+ <div style={{ color: migrationStatus.includes('成功') ? '#43a047' : '#e53935', fontSize: 14, marginTop: 8 }}>
+ {migrationStatus}
+ </div>
+ )}
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={handleMigrationSubmit} variant="contained" color="primary">提交迁移</Button>
+ <Button onClick={() => setMigrationOpen(false)} variant="outlined">取消</Button>
+ </DialogActions>
</Dialog>
</div>
);