blob: 074d68c13eb5d3f97e66013d45cc6b6d84406b6c [file] [log] [blame]
rhjc6a4ee02025-06-06 00:45:18 +08001import React, { useState, useEffect } from "react";
956303669a32fc2c2025-06-02 19:45:53 +08002import AccountCircleIcon from "@mui/icons-material/AccountCircle";
wht30587822025-06-09 23:33:09 +08003import PersonIcon from "@mui/icons-material/Person";
4import EmailIcon from "@mui/icons-material/Email";
5import SchoolIcon from "@mui/icons-material/School";
6import CloudUploadIcon from "@mui/icons-material/CloudUpload";
7import CloudDownloadIcon from "@mui/icons-material/CloudDownload";
8import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
9import FavoriteIcon from "@mui/icons-material/Favorite";
10import EmptyIcon from "@mui/icons-material/Inbox";
wht15642182025-06-08 00:16:52 +080011import Button from '@mui/material/Button';
12import TextField from '@mui/material/TextField';
13import MenuItem from '@mui/material/MenuItem';
14import Dialog from '@mui/material/Dialog';
15import DialogTitle from '@mui/material/DialogTitle';
16import DialogContent from '@mui/material/DialogContent';
17import DialogActions from '@mui/material/DialogActions';
956303669a32fc2c2025-06-02 19:45:53 +080018import { useNavigate } from "react-router-dom";
223011330f9623f2025-06-06 00:22:05 +080019import { API_BASE_URL } from "./config";
wht30587822025-06-09 23:33:09 +080020import "./UserProfile.css";
956303669a32fc2c2025-06-02 19:45:53 +080021
22export default function UserProfile() {
23 const navigate = useNavigate();
24 const [userInfo, setUserInfo] = useState({
rhjc6a4ee02025-06-06 00:45:18 +080025 avatar_url: "",
956303669a32fc2c2025-06-02 19:45:53 +080026 username: "示例用户",
27 email: "user@example.com",
223011339e292152025-06-08 00:34:37 +080028 invitetimes: "",
956303669a32fc2c2025-06-02 19:45:53 +080029 school: "",
rhjc6a4ee02025-06-06 00:45:18 +080030 account_status: "",
31 gender: "",
956303669a32fc2c2025-06-02 19:45:53 +080032 });
33 const [tempUserInfo, setTempUserInfo] = useState({ ...userInfo });
rhjc6a4ee02025-06-06 00:45:18 +080034 const [userSeeds, setUserSeeds] = useState([]);
wht15642182025-06-08 00:16:52 +080035 const [userFavorites, setUserFavorites] = useState([]);
rhjc6a4ee02025-06-06 00:45:18 +080036 const [userStats, setUserStats] = useState({
37 magic: 0,
38 upload: 0,
wht2bf8f802025-06-08 15:52:18 +080039 viptime: 0,
rhjc6a4ee02025-06-06 00:45:18 +080040 ratio: 0,
41 });
42
wht15642182025-06-08 00:16:52 +080043 // 邀请相关
44 const [inviteEmail, setInviteEmail] = useState('');
45 const [inviteStatus, setInviteStatus] = useState('');
46
47 // 兑换相关
48 const [exchangeType, setExchangeType] = useState('uploaded');
49 const [exchangeMagic, setExchangeMagic] = useState('');
50 const [exchangeResult, setExchangeResult] = useState(0);
51
52 // 兑换比例
wht2bf8f802025-06-08 15:52:18 +080053 const exchangeRate = { uploaded: 0.1, downloaded: 0.1, vip_downloads: 100 };
wht15642182025-06-08 00:16:52 +080054
55 // 用户申诉相关
56 const [appealOpen, setAppealOpen] = useState(false);
57 const [appealTitle, setAppealTitle] = useState('');
58 const [appealFile, setAppealFile] = useState(null);
59
wht2bf8f802025-06-08 15:52:18 +080060 // 账号迁移相关
61 const [migrationOpen, setMigrationOpen] = useState(false);
wht338fc032025-06-09 17:16:22 +080062 const [migrationUpload, setMigrationUpload] = useState('');
wht2bf8f802025-06-08 15:52:18 +080063 const [migrationEmail, setMigrationEmail] = useState('');
64 const [migrationPassword, setMigrationPassword] = useState('');
65 const [migrationStatus, setMigrationStatus] = useState('');
wht338fc032025-06-09 17:16:22 +080066
wht15642182025-06-08 00:16:52 +080067 // 兑换结果计算
68 React.useEffect(() => {
69 if (!exchangeMagic || isNaN(exchangeMagic)) {
70 setExchangeResult(0);
71 return;
72 }
73 setExchangeResult(Number(exchangeMagic) / exchangeRate[exchangeType]);
74 }, [exchangeMagic, exchangeType]);
75
76 // 获取用户信息
rhjc6a4ee02025-06-06 00:45:18 +080077 useEffect(() => {
78 const fetchUserInfo = async () => {
223011339e292152025-06-08 00:34:37 +080079 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
wht338fc032025-06-09 17:16:22 +080080 const userid = match ? match[2] : null;
rhjc6a4ee02025-06-06 00:45:18 +080081 if (!userid) return;
82 try {
223011330f9623f2025-06-06 00:22:05 +080083 const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`);
rhjc6a4ee02025-06-06 00:45:18 +080084 if (res.ok) {
85 const data = await res.json();
86 setUserInfo(data);
87 setTempUserInfo(data);
88 }
89 } catch (err) {
rhjc6a4ee02025-06-06 00:45:18 +080090 console.error("获取用户信息失败", err);
91 }
92 };
93 fetchUserInfo();
94 }, []);
95
rhjc6a4ee02025-06-06 00:45:18 +080096 useEffect(() => {
97 const fetchUserSeeds = async () => {
223011339e292152025-06-08 00:34:37 +080098 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
wht338fc032025-06-09 17:16:22 +080099 const userid = match ? match[2] : null;
rhjc6a4ee02025-06-06 00:45:18 +0800100 if (!userid) return;
101 try {
223011330f9623f2025-06-06 00:22:05 +0800102 const res = await fetch(`${API_BASE_URL}/api/user-seeds?userid=${userid}`);
rhjc6a4ee02025-06-06 00:45:18 +0800103 if (res.ok) {
104 const data = await res.json();
105 setUserSeeds(data);
106 }
107 } catch (err) {
108 console.error("获取种子列表失败", err);
109 }
110 };
111 fetchUserSeeds();
112 }, []);
wht338fc032025-06-09 17:16:22 +0800113
wht15642182025-06-08 00:16:52 +0800114 useEffect(() => {
wht2bf8f802025-06-08 15:52:18 +0800115 const fetchUserFavorites = async () => {
116 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
117 const userid = match ? match[2] : null;
118 if (!userid) return;
119 try {
120 const res = await fetch(`${API_BASE_URL}/api/user-favorites?userid=${userid}`);
121 if (res.ok) {
122 const data = await res.json();
123 // console.log("获取收藏种子列表:", data);
124 setUserFavorites(data);
125 }
126 } catch (err) {
127 console.error("获取收藏种子列表失败", err);
128 }
129 };
130 fetchUserFavorites();
wht15642182025-06-08 00:16:52 +0800131 }, []);
wht338fc032025-06-09 17:16:22 +0800132
rhjc6a4ee02025-06-06 00:45:18 +0800133 useEffect(() => {
134 const fetchUserStats = async () => {
wht2bf8f802025-06-08 15:52:18 +0800135 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
136 const userid = match ? match[2] : null;
rhjc6a4ee02025-06-06 00:45:18 +0800137 if (!userid) return;
138 try {
223011330f9623f2025-06-06 00:22:05 +0800139 const res = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);
rhjc6a4ee02025-06-06 00:45:18 +0800140 if (res.ok) {
141 const data = await res.json();
142 setUserStats(data);
143 }
144 } catch (err) {
145 console.error("获取活跃度信息失败", err);
146 }
147 };
148 fetchUserStats();
149 }, []);
956303669a32fc2c2025-06-02 19:45:53 +0800150
151 const handleInputChange = (field, value) => {
152 setTempUserInfo({ ...tempUserInfo, [field]: value });
153 };
154
rhjc6a4ee02025-06-06 00:45:18 +0800155 const handleSave = async () => {
wht338fc032025-06-09 17:16:22 +0800156 if (tempUserInfo.gender === "男") {
223011330f9623f2025-06-06 00:22:05 +0800157 tempUserInfo.gender = "m";
wht338fc032025-06-09 17:16:22 +0800158 } else if (tempUserInfo.gender === "女") {
223011330f9623f2025-06-06 00:22:05 +0800159 tempUserInfo.gender = "f";
160 }
956303669a32fc2c2025-06-02 19:45:53 +0800161 setUserInfo({ ...tempUserInfo });
wht338fc032025-06-09 17:16:22 +0800162
223011339e292152025-06-08 00:34:37 +0800163 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
164 const userid = match ? match[2] : null;
rhjc6a4ee02025-06-06 00:45:18 +0800165 try {
223011330f9623f2025-06-06 00:22:05 +0800166 const res = await fetch(`${API_BASE_URL}/api/change-profile`, {
rhjc6a4ee02025-06-06 00:45:18 +0800167 method: 'POST',
168 headers: {
169 'Content-Type': 'application/json',
170 },
171 body: JSON.stringify({ userid, ...tempUserInfo }),
172 });
173 if (res.ok) {
174 alert("信息已保存!");
175 } else {
176 alert("保存失败,请重试。");
177 }
178 } catch (err) {
179 alert("保存失败,请检查网络连接。");
180 console.error("保存用户信息失败", err);
181 }
956303669a32fc2c2025-06-02 19:45:53 +0800182 };
183
184 const handleAvatarClick = () => {
223011339e292152025-06-08 00:34:37 +0800185 const avatarUrl = prompt("请输入头像的URL:");
186 if (avatarUrl) {
187 setTempUserInfo({ ...tempUserInfo, avatar_url: avatarUrl });
956303669a32fc2c2025-06-02 19:45:53 +0800188 }
189 };
190
wht15642182025-06-08 00:16:52 +0800191 // 邀请
wht2bf8f802025-06-08 15:52:18 +0800192 const handleInvite = async () => {
193 if (!inviteEmail) {
194 setInviteStatus("请输入邀请邮箱");
195 return;
196 }
197 // 获取userid
198 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
199 const userid = match ? match[2] : null;
200 if (!userid) {
201 setInviteStatus("未获取到用户ID");
202 return;
203 }
wht15642182025-06-08 00:16:52 +0800204 if (userInfo.invite_left <= 0) {
205 setInviteStatus("邀请次数已用完");
206 return;
207 }
wht2bf8f802025-06-08 15:52:18 +0800208 try {
wht338fc032025-06-09 17:16:22 +0800209 const res = await fetch(`${API_BASE_URL}/api/invite`, {
wht2bf8f802025-06-08 15:52:18 +0800210 method: 'POST',
211 headers: { 'Content-Type': 'application/json' },
212 body: JSON.stringify({ userid, invite_email: inviteEmail }),
213 });
214 if (res.ok) {
215 const data = await res.json();
216 setInviteStatus("邀请成功");
217 // 更新剩余次数
218 const left = data.invite_left !== undefined ? data.invite_left : userInfo.invite_left - 1;
219 setUserInfo(prev => ({ ...prev, invite_left: left }));
220 setTempUserInfo(prev => ({ ...prev, invite_left: left }));
221 setInviteEmail('');
222 } else {
223 const errorText = await res.text();
224 setInviteStatus("邀请失败:" + errorText);
225 }
226 } catch (err) {
227 console.error("邀请失败", err);
228 setInviteStatus("邀请失败,请检查网络");
229 }
wht338fc032025-06-09 17:16:22 +0800230 };
231
232 // 兑换魔力值
wht2bf8f802025-06-08 15:52:18 +0800233 const handleExchange = async () => {
wht15642182025-06-08 00:16:52 +0800234 const magic = Number(exchangeMagic);
235 if (!magic || isNaN(magic) || magic <= 0) return;
236 if (magic > userStats.magic) {
237 alert("魔力值不足!");
238 return;
239 }
wht338fc032025-06-09 17:16:22 +0800240
wht2bf8f802025-06-08 15:52:18 +0800241 // 检查兑换结果是否为整数
242 const calculatedExchangeResult = magic / exchangeRate[exchangeType];
243 if (!Number.isInteger(calculatedExchangeResult)) {
244 alert("兑换结果必须为整数,请调整魔力值!");
245 return;
wht15642182025-06-08 00:16:52 +0800246 }
wht338fc032025-06-09 17:16:22 +0800247
wht2bf8f802025-06-08 15:52:18 +0800248 // 获取userid
249 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
250 const userid = match ? match[2] : null;
251 if (!userid) {
252 alert("未获取到用户ID");
253 return;
254 }
wht15642182025-06-08 00:16:52 +0800255
wht2bf8f802025-06-08 15:52:18 +0800256 console.log("兑换请求参数:", { userid, magic, exchangeType, exchangeResult: calculatedExchangeResult });
257 try {
258 // 发送兑换请求到后端
259 const res = await fetch(`${API_BASE_URL}/api/exchange`, {
260 method: 'POST',
261 headers: { 'Content-Type': 'application/json' },
wht338fc032025-06-09 17:16:22 +0800262 body: JSON.stringify({
263 userid,
264 magic,
wht2bf8f802025-06-08 15:52:18 +0800265 exchangeType,
266 exchangeResult: calculatedExchangeResult
267 }),
268 });
269 // console.log("兑换请求结果:", res);
270 if (res.ok) {
271 // 兑换成功后重新获取用户数据
272 const statsRes = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);
273 if (statsRes.ok) {
274 const updatedStats = await statsRes.json();
275 setUserStats(updatedStats);
276 }
277 setExchangeMagic('');
278 alert("兑换成功!");
279 } else {
280 const errorText = await res.text();
281 alert("兑换失败:" + errorText);
282 }
283 } catch (err) {
284 console.error("兑换失败", err);
285 alert("兑换失败,请检查网络");
286 }
287 };
wht15642182025-06-08 00:16:52 +0800288 // 删除种子
289 const handleDeleteSeed = (seedid) => {
290 setUserSeeds(userSeeds.filter((s) => s.seedid !== seedid));
291 };
292
wht2bf8f802025-06-08 15:52:18 +0800293 // 取消收藏
294 const handleRemoveFavorite = async (seedid) => {
295 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
296 const userid = match ? match[2] : null;
297 if (!userid) {
298 alert('未获取到用户ID');
299 return;
300 }
301 try {
302 const res = await fetch(`${API_BASE_URL}/api/remove-favorite`, {
303 method: 'POST',
304 headers: { 'Content-Type': 'application/json' },
305 body: JSON.stringify({ userid, seedid }),
wht338fc032025-06-09 17:16:22 +0800306 }); if (res.ok) {
wht2bf8f802025-06-08 15:52:18 +0800307 setUserFavorites(userFavorites.filter((s) => (s.seedid || s.seed_id) !== seedid));
308 alert('已取消收藏');
309 } else {
310 alert('取消收藏失败,请重试');
311 }
312 } catch (err) {
313 console.error('取消收藏失败', err);
314 alert('取消收藏失败,请检查网络');
315 }
316 };
317
wht15642182025-06-08 00:16:52 +0800318 // 申诉提交逻辑
wht2bf8f802025-06-08 15:52:18 +0800319 const handleAppealSubmit = async () => {
320 if (!appealTitle || !appealFile) return;
wht2bf8f802025-06-08 15:52:18 +0800321 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
322 const userid = match ? match[2] : null;
323 if (!userid) {
324 alert('未获取到用户ID');
325 return;
326 }
wht338fc032025-06-09 17:16:22 +0800327
wht2bf8f802025-06-08 15:52:18 +0800328 const formData = new FormData();
329 formData.append('userid', userid);
330 formData.append('content', appealTitle);
331 formData.append('file', appealFile);
332 try {
333 const res = await fetch(`${API_BASE_URL}/api/submit-appeal`, {
334 method: 'POST',
335 body: formData,
336 });
337 if (res.ok) {
338 alert('申诉已提交');
339 setAppealOpen(false);
340 setAppealTitle('');
341 setAppealFile(null);
342 } else {
343 const errorText = await res.text();
344 alert('申诉失败:' + errorText);
345 }
346 } catch (err) {
347 console.error('申诉失败', err);
348 alert('申诉失败,请检查网络');
349 }
350 };
wht338fc032025-06-09 17:16:22 +0800351
352 // 账号迁移
wht2bf8f802025-06-08 15:52:18 +0800353 const handleMigrationSubmit = async () => {
354 if (!appealFile) {
355 setMigrationStatus('请选择PDF文件');
356 return;
357 }
wht338fc032025-06-09 17:16:22 +0800358 if (!migrationUpload || isNaN(migrationUpload) || Number(migrationUpload) <= 0) {
359 setMigrationStatus('请输入有效的待发放上传量');
360 return;
361 }
362
wht2bf8f802025-06-08 15:52:18 +0800363 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
364 const currentUserId = match ? match[2] : null;
365 if (!currentUserId) {
366 setMigrationStatus('未获取到当前用户ID');
367 return;
368 }
wht338fc032025-06-09 17:16:22 +0800369
wht2bf8f802025-06-08 15:52:18 +0800370 try {
wht2bf8f802025-06-08 15:52:18 +0800371 const formData = new FormData();
372 formData.append('userid', currentUserId);
373 formData.append('file', appealFile);
wht338fc032025-06-09 17:16:22 +0800374 formData.append('uploadtogive', migrationUpload);
375
wht2bf8f802025-06-08 15:52:18 +0800376 const res = await fetch(`${API_BASE_URL}/api/migrate-account`, {
377 method: 'POST',
378 body: formData,
379 });
wht338fc032025-06-09 17:16:22 +0800380
wht2bf8f802025-06-08 15:52:18 +0800381 if (res.ok) {
382 setMigrationStatus('账号迁移申请已提交,请等待管理员审核');
383 setTimeout(() => {
384 setMigrationOpen(false);
385 setAppealFile(null);
wht338fc032025-06-09 17:16:22 +0800386 setMigrationUpload('');
wht2bf8f802025-06-08 15:52:18 +0800387 setMigrationStatus('');
388 }, 2000);
389 } else {
390 const errorText = await res.text();
391 setMigrationStatus('迁移失败:' + errorText);
392 }
393 } catch (err) {
394 console.error('账号迁移失败', err);
395 setMigrationStatus('迁移失败,请检查网络');
396 }
wht30587822025-06-09 23:33:09 +0800397 }; return (
398 <div className="user-profile-container">
399 <div className="profile-grid">
400 {/* 用户基本信息卡片 */}
401 <div className="profile-card user-info-card">
402 <div className="user-avatar-section">
403 <div className="avatar-container" onClick={handleAvatarClick}>
404 {tempUserInfo.avatar_url ? (
405 <img
406 src={tempUserInfo.avatar_url}
407 alt="用户头像"
408 className="user-avatar"
409 />
410 ) : (
411 <AccountCircleIcon style={{ fontSize: 120, color: '#1a237e' }} />
412 )}
413 <div className="avatar-overlay">
414 <span>点击更换头像</span>
415 </div>
416 </div>
417 <h2 className="user-title">用户个人资料</h2>
418 </div>
wht15642182025-06-08 00:16:52 +0800419
wht30587822025-06-09 23:33:09 +0800420 <div className="user-form">
421 <div className="form-group">
422 <label className="form-label">
423 <PersonIcon style={{ fontSize: 16, marginRight: 4 }} />
424 用户名
425 </label>
426 <input
427 className="form-input"
428 value={tempUserInfo.username}
429 onChange={(e) => handleInputChange("username", e.target.value)}
430 placeholder="请输入用户名"
956303669a32fc2c2025-06-02 19:45:53 +0800431 />
wht30587822025-06-09 23:33:09 +0800432 </div>
433
434 <div className="form-group">
435 <label className="form-label">
436 <EmailIcon style={{ fontSize: 16, marginRight: 4 }} />
437 邮箱
438 </label>
439 <input
440 className="form-input"
441 value={tempUserInfo.email}
442 readOnly
443 style={{ cursor: 'not-allowed' }}
444 />
445 </div>
446
447 <div className="form-group">
448 <label className="form-label">
449 <SchoolIcon style={{ fontSize: 16, marginRight: 4 }} />
450 学校
451 </label>
452 <input
453 className="form-input"
454 value={tempUserInfo.school}
455 onChange={(e) => handleInputChange("school", e.target.value)}
456 placeholder="请输入学校名称"
457 />
458 </div>
459
460 <div className="form-group">
461 <label className="form-label">账号状态</label>
462 <div className="status-indicator">
463 <input
464 className="form-input"
465 value={tempUserInfo.account_status === 1 || tempUserInfo.account_status === "1" ? "封禁" : "正常"}
466 readOnly
467 />
468 <div className={`status-dot ${tempUserInfo.account_status === 1 || tempUserInfo.account_status === "1" ? 'banned' : 'active'}`}></div>
469 </div>
470 </div>
471
472 <div className="form-group">
473 <label className="form-label">性别</label>
474 <select
475 className="form-input"
476 value={tempUserInfo.gender}
477 onChange={(e) => handleInputChange("gender", e.target.value)}
478 >
479 <option value="">请选择性别</option>
480 <option value="m">男性</option>
481 <option value="f">女性</option>
482 </select>
483 </div>
484
485 {/* 邀请功能 */}
486 <div className="invite-section">
487 <label className="form-label">邀请功能</label>
488 <div className="invite-form">
489 <input
490 className="form-input invite-input"
491 type="email"
492 placeholder="输入邀请邮箱"
493 value={inviteEmail}
494 onChange={e => setInviteEmail(e.target.value)}
495 disabled={Number(tempUserInfo.invite_left) === 0}
496 />
497 <button
498 className="btn btn-primary btn-small"
499 onClick={handleInvite}
500 disabled={Number(tempUserInfo.invite_left) === 0 || !inviteEmail}
501 >
502 邀请
503 </button>
504 </div>
505 <div className="invite-counter">
506 剩余邀请次数:{tempUserInfo.invite_left || "0"}
507 </div>
508 {inviteStatus && (
509 <div className="invite-status">{inviteStatus}</div>
510 )}
511 </div>
512
513 <div className="btn-group">
514 <button className="btn btn-primary" onClick={handleSave}>
515 保存信息
516 </button>
517 <button className="btn btn-danger" onClick={() => setAppealOpen(true)}>
518 用户申诉
519 </button>
520 <button className="btn btn-warning" onClick={() => setMigrationOpen(true)}>
521 账号迁移
522 </button> </div>
523 </div>
524 </div>
525
526 {/* 活跃度卡片 */}
527 <div className="profile-card activity-card">
528 <h3 className="activity-title">
529 <AutoAwesomeIcon style={{ fontSize: 24, marginRight: 8 }} />
530 活跃度统计
531 </h3>
532
533 <div className="activity-content">
534 {/* 魔力值兑换 */}
535 <div className="magic-exchange">
536 <div className="stat-item">
537 <span className="stat-label">当前魔力值</span>
538 <span className="stat-value magic">{userStats.magic}</span>
539 </div>
540
541 <div className="exchange-form">
542 <input
543 className="form-input exchange-input"
544 type="number"
545 placeholder="输入兑换魔力值"
546 value={exchangeMagic}
547 onChange={e => setExchangeMagic(e.target.value)}
548 />
549 <select
550 className="form-input exchange-input"
551 value={exchangeType}
552 onChange={e => setExchangeType(e.target.value)}
553 >
554 <option value="uploaded">上传量(增加)</option>
555 <option value="downloaded">下载量(减少)</option>
556 <option value="vip_downloads">VIP下载次数(增加)</option>
557 </select>
558 <button
559 className="btn btn-primary btn-small"
560 onClick={handleExchange}
561 disabled={
562 !exchangeMagic ||
563 isNaN(exchangeMagic) ||
564 Number(exchangeMagic) <= 0 ||
565 Number(exchangeMagic) > userStats.magic ||
566 !Number.isInteger(exchangeResult)
567 }
568 >
569 兑换
570 </button>
571 </div>
572
573 {exchangeMagic && (
574 <div className="exchange-result">
575 可兑换:{exchangeResult} {exchangeType === 'vip_downloads' ? '次' : 'MB'}
576 {!Number.isInteger(exchangeResult) && exchangeResult > 0 && (
577 <span style={{ color: '#e53935', marginLeft: 8 }}>
578 (结果必须为整数)
579 </span>
580 )}
581 </div>
582 )}
583 </div>
584
585 {/* 统计数据 */}
586 <div className="stats-grid">
587 <div className="stat-item">
588 <span className="stat-label">
589 <CloudUploadIcon style={{ fontSize: 16, marginRight: 4 }} />
590 上传量
591 </span>
592 <span className="stat-value upload">
593 {(userStats.upload / 1000000)?.toFixed(2)} MB
594 </span>
595 </div>
596
597 <div className="stat-item">
598 <span className="stat-label">
599 <CloudDownloadIcon style={{ fontSize: 16, marginRight: 4 }} />
600 下载量
601 </span>
602 <span className="stat-value download">
603 {(userStats.download / 1000000)?.toFixed(2)} MB
604 </span>
605 </div>
606
607 <div className="stat-item">
608 <span className="stat-label">上传/下载比</span>
609 <span className="stat-value ratio">
610 {userStats.download === 0 ? "∞" : (userStats.upload / userStats.download).toFixed(2)}
611 </span>
612 </div>
613
614 <div className="stat-item">
615 <span className="stat-label">VIP下载次数</span>
616 <span className="stat-value vip">{userStats.viptime}</span>
617 </div>
618 </div>
619 </div>
620 </div> {/* 个人上传种子列表 */}
621 <div className="profile-card seeds-card">
622 <h3 className="list-title">
623 <CloudUploadIcon style={{ fontSize: 24, marginRight: 8 }} />
624 个人上传种子列表
625 </h3>
626
627 <div className="list-container">
628 {userSeeds.length === 0 ? (
629 <div className="empty-state">
630 <EmptyIcon className="empty-icon" />
631 <span>暂无上传种子</span>
632 </div>
633 ) : (
634 <ul className="seeds-list">
635 {userSeeds.map((seed, idx) => (
636 <li
637 key={seed.seedid || idx}
638 className="seed-item"
639 onClick={e => {
640 if (e.target.classList.contains('delete-btn')) return;
641 navigate(`/torrent/${seed.seed_id}`);
642 }}
643 >
644 <span className="seed-title">{seed.title}</span>
645 <span className="seed-tags">{seed.tags}</span>
646 <span className="seed-stats">人气: {seed.downloadtimes}</span>
647 <div className="seed-actions">
648 <button
649 className="btn btn-danger btn-small delete-btn"
650 onClick={async e => {
651 e.stopPropagation();
652 const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
653 const userid = match ? match[2] : null;
654 if (!userid) {
655 alert('未获取到用户ID');
656 return;
657 }
658 try {
659 const res = await fetch(`${API_BASE_URL}/api/delete-seed`, {
660 method: 'POST',
661 headers: { 'Content-Type': 'application/json' },
662 body: JSON.stringify({ seed_id: seed.seed_id, userid }),
663 });
664 if (res.ok) {
665 setUserSeeds(userSeeds.filter((s, i) => (s.seed_id || i) !== (seed.seed_id || idx)));
666 } else {
667 alert('删除失败,请重试');
668 }
669 } catch (err) {
670 alert('删除失败,请检查网络');
671 }
672 }}
673 >
674 删除
675 </button>
676 </div>
677 </li>
678 ))}
679 </ul>
956303669a32fc2c2025-06-02 19:45:53 +0800680 )}
681 </div>
956303669a32fc2c2025-06-02 19:45:53 +0800682 </div>
wht30587822025-06-09 23:33:09 +0800683
684 {/* 个人收藏种子列表 */}
685 <div className="profile-card favorites-card">
686 <h3 className="list-title">
687 <FavoriteIcon style={{ fontSize: 24, marginRight: 8 }} />
688 个人收藏种子列表
689 </h3>
690
691 <div className="list-container">
692 {userFavorites.length === 0 ? (
693 <div className="empty-state">
694 <FavoriteIcon className="empty-icon" />
695 <span>暂无收藏种子</span>
696 </div>
697 ) : (
698 <ul className="seeds-list">
699 {userFavorites.map((seed, idx) => (
700 <li
701 key={seed.seedid || idx}
702 className="seed-item"
703 onClick={e => {
704 if (e.target.classList.contains('remove-favorite-btn')) return;
705 navigate(`/torrent/${seed.seedid || seed.seed_id}`);
rhjc6a4ee02025-06-06 00:45:18 +0800706 }}
wht30587822025-06-09 23:33:09 +0800707 >
708 <span className="seed-title">{seed.seed.title}</span>
709 <span className="seed-tags">{seed.seed.tags}</span>
710 <span className="seed-stats">人气: {seed.seed.downloadtimes}</span>
711 <div className="seed-actions">
712 <button
713 className="btn btn-warning btn-small remove-favorite-btn"
714 onClick={e => {
715 e.stopPropagation();
716 handleRemoveFavorite(seed.seedid || seed.seed_id);
717 }}
718 >
719 取消收藏
720 </button>
721 </div>
722 </li>
723 ))}
724 </ul>
725 )}
726 </div>
956303669a32fc2c2025-06-02 19:45:53 +0800727 </div>
wht30587822025-06-09 23:33:09 +0800728 </div>
wht15642182025-06-08 00:16:52 +0800729 {/* 申诉弹窗 */}
wht30587822025-06-09 23:33:09 +0800730 <Dialog open={appealOpen} onClose={() => setAppealOpen(false)} maxWidth="sm" fullWidth>
731 <DialogTitle style={{ background: '#f8faff', color: '#1a237e', fontWeight: 600 }}>
732 提交申诉
733 </DialogTitle>
734 <DialogContent style={{ padding: '24px', background: '#ffffff' }}>
735 <div style={{ marginBottom: 20 }}>
wht15642182025-06-08 00:16:52 +0800736 <TextField
737 label="申诉主题"
738 fullWidth
739 value={appealTitle}
740 onChange={e => setAppealTitle(e.target.value)}
wht30587822025-06-09 23:33:09 +0800741 variant="outlined"
742 style={{ marginBottom: 16 }}
wht338fc032025-06-09 17:16:22 +0800743 />
744 </div>
745 <div>
wht2bf8f802025-06-08 15:52:18 +0800746 <input
747 type="file"
748 accept=".pdf"
749 onChange={e => {
750 const file = e.target.files[0];
751 if (file && file.type !== 'application/pdf') {
752 alert('请选择PDF文件');
753 e.target.value = '';
754 setAppealFile(null);
755 } else {
756 setAppealFile(file);
757 }
758 }}
wht30587822025-06-09 23:33:09 +0800759 style={{
760 marginTop: 8,
761 padding: '12px',
762 border: '2px dashed #e0e7ff',
763 borderRadius: '8px',
764 width: '100%',
765 background: '#f8faff'
766 }}
wht2bf8f802025-06-08 15:52:18 +0800767 />
wht30587822025-06-09 23:33:09 +0800768 <div style={{
769 fontSize: 12,
770 color: '#666',
771 marginTop: 8,
772 padding: '8px 12px',
773 background: '#f0f4ff',
774 borderRadius: '6px'
775 }}>
776 请选择PDF文件(最大100MB
777 </div>
778 </div>
779 </DialogContent>
780 <DialogActions style={{ padding: '16px 24px', background: '#f8faff' }}>
781 <Button
782 onClick={handleAppealSubmit}
783 variant="contained"
784 disabled={!appealTitle || !appealFile}
785 style={{
786 background: (!appealTitle || !appealFile) ? '#ccc' : 'linear-gradient(135deg, #1a237e 0%, #3f51b5 100%)',
787 color: 'white',
788 fontWeight: 600
789 }}
790 >
791 提交申诉
792 </Button>
793 <Button
794 onClick={() => setAppealOpen(false)}
795 variant="outlined"
796 style={{ color: '#1a237e', borderColor: '#1a237e' }}
797 >
798 取消
799 </Button>
800 </DialogActions>
801 </Dialog>
802
803 {/* 账号迁移弹窗 */}
804 <Dialog open={migrationOpen} onClose={() => setMigrationOpen(false)} maxWidth="sm" fullWidth>
805 <DialogTitle style={{ background: '#f8faff', color: '#1a237e', fontWeight: 600 }}>
806 账号迁移
807 </DialogTitle>
808 <DialogContent style={{ padding: '24px', background: '#ffffff' }}>
809 <div style={{ marginBottom: 20 }}>
810 <TextField
811 label="待发放上传量"
812 type="number"
813 fullWidth
814 value={migrationUpload}
815 onChange={e => setMigrationUpload(e.target.value)}
816 variant="outlined"
817 inputProps={{ min: 1 }}
818 style={{ marginBottom: 16 }}
819 />
820 </div>
821 <div>
822 <input
823 type="file"
824 accept=".pdf"
825 onChange={e => {
826 const file = e.target.files[0];
827 if (file && file.type !== 'application/pdf') {
828 alert('请选择PDF文件');
829 e.target.value = '';
830 setAppealFile(null);
831 } else {
832 setAppealFile(file);
833 }
834 }}
835 style={{
836 marginTop: 8,
837 padding: '12px',
838 border: '2px dashed #e0e7ff',
839 borderRadius: '8px',
840 width: '100%',
841 background: '#f8faff'
842 }}
843 />
844 <div style={{
845 fontSize: 12,
846 color: '#666',
847 marginTop: 8,
848 padding: '8px 12px',
849 background: '#f0f4ff',
850 borderRadius: '6px'
851 }}>
wht2bf8f802025-06-08 15:52:18 +0800852 请选择PDF文件(最大10MB
853 </div>
854 </div>
855 {migrationStatus && (
wht30587822025-06-09 23:33:09 +0800856 <div style={{
857 color: migrationStatus.includes('成功') ? '#43a047' : '#e53935',
858 fontSize: 14,
859 marginTop: 16,
860 padding: '12px',
861 borderRadius: '8px',
862 background: migrationStatus.includes('成功') ? 'rgba(67, 160, 71, 0.1)' : 'rgba(229, 57, 53, 0.1)',
863 border: `1px solid ${migrationStatus.includes('成功') ? 'rgba(67, 160, 71, 0.3)' : 'rgba(229, 57, 53, 0.3)'}`
864 }}>
wht2bf8f802025-06-08 15:52:18 +0800865 {migrationStatus}
866 </div>
867 )}
868 </DialogContent>
wht30587822025-06-09 23:33:09 +0800869 <DialogActions style={{ padding: '16px 24px', background: '#f8faff' }}>
870 <Button
871 onClick={handleMigrationSubmit}
872 variant="contained"
873 style={{
874 background: 'linear-gradient(135deg, #ff9800 0%, #ffa726 100%)',
875 color: 'white',
876 fontWeight: 600
877 }}
878 >
879 提交迁移
880 </Button>
881 <Button
882 onClick={() => setMigrationOpen(false)}
883 variant="outlined"
884 style={{ color: '#1a237e', borderColor: '#1a237e' }}
885 >
886 取消
887 </Button>
wht2bf8f802025-06-08 15:52:18 +0800888 </DialogActions>
wht15642182025-06-08 00:16:52 +0800889 </Dialog>
956303669a32fc2c2025-06-02 19:45:53 +0800890 </div>
891 );
892}