blob: 04020598ba5f5df54f45c8e4a154ac01e9883675 [file] [log] [blame]
Seamherd6c950f2025-06-09 23:48:28 +08001"use client";
lfmylbhf789e7172025-06-06 18:37:21 +08002
Seamherd6c950f2025-06-09 23:48:28 +08003import React, { useEffect, useState, useRef } from "react";
4import { TabView, TabPanel } from "primereact/tabview";
5import { Avatar } from "primereact/avatar";
6import { Button } from "primereact/button";
7import { Card } from "primereact/card";
LaoeGaocid0773912025-06-09 00:38:40 +08008import { Image } from "primereact/image";
lfmylbhf1783a092025-06-07 20:05:22 +08009// 发布资源
Seamherd6c950f2025-06-09 23:48:28 +080010import { Dialog } from "primereact/dialog";
LaoeGaocid0773912025-06-09 00:38:40 +080011import { InputText } from "primereact/inputtext";
12import { InputTextarea } from "primereact/inputtextarea";
13import { FileUpload } from "primereact/fileupload";
lfmylbhf1783a092025-06-07 20:05:22 +080014// 资源分类
15import { RadioButton, RadioButtonChangeEvent } from "primereact/radiobutton";
16// 资源标签
Seamherd6c950f2025-06-09 23:48:28 +080017import { MultiSelect, MultiSelectChangeEvent } from "primereact/multiselect";
lfmylbhf789e7172025-06-06 18:37:21 +080018// 浮动按钮
Seamherd6c950f2025-06-09 23:48:28 +080019import { SpeedDial } from "primereact/speeddial";
lfmylbhf789e7172025-06-06 18:37:21 +080020// 评分图标
Seamherd6c950f2025-06-09 23:48:28 +080021import { Fire } from "@icon-park/react";
lfmylbhf789e7172025-06-06 18:37:21 +080022// 消息提醒
Seamherd6c950f2025-06-09 23:48:28 +080023import { Toast } from "primereact/toast";
lfmylbhf789e7172025-06-06 18:37:21 +080024// 页面跳转
25import { useRouter } from "next/navigation";
lfmylbhfa9fa5c82025-06-09 00:34:49 +080026// 类型转换
LaoeGaoci68253852025-06-09 22:42:18 +080027import { toNumber } from "lodash";
lfmylbhfa9fa5c82025-06-09 00:34:49 +080028// 分页
Seamherd6c950f2025-06-09 23:48:28 +080029import { Paginator, type PaginatorPageChangeEvent } from "primereact/paginator";
lfmylbhf789e7172025-06-06 18:37:21 +080030
31// 接口传输
32import axios from "axios";
Seamherd6c950f2025-06-09 23:48:28 +080033import { useLocalStorage } from "../hook/useLocalStorage";
lfmylbhf789e7172025-06-06 18:37:21 +080034// 样式
Seamherd6c950f2025-06-09 23:48:28 +080035import "./user.scss";
lfmylbhf789e7172025-06-06 18:37:21 +080036
LaoeGaocid0773912025-06-09 00:38:40 +080037interface User {
Seamherd6c950f2025-06-09 23:48:28 +080038 Id: number;
39}
LaoeGaocid0773912025-06-09 00:38:40 +080040
lfmylbhf789e7172025-06-06 18:37:21 +080041// 用户信息
42interface UserInfo {
Seamherd6c950f2025-06-09 23:48:28 +080043 userId: number;
44 username: string;
45 password: string;
46 avatar: string;
47 followerCount: number; // 粉丝数
48 subscriberCount: number; // 关注数
49 signature: string; // 个性签名
50 uploadAmount: number;
51 purchaseAmount: number;
52 credits: number;
lfmylbhf789e7172025-06-06 18:37:21 +080053}
54
55// 用户数据
56interface UserData {
Seamherd6c950f2025-06-09 23:48:28 +080057 subscriberCount: number; // 关注数
58 uploadAmount: number; // 上传量(资源个数)
59 beDownloadedAmount: number; // 上传资源被下载量
60 seedPercentageList: number[]; // 上传资源类型百分比列表,按材质包、模组、整合包、地图的顺序返回
lfmylbhf789e7172025-06-06 18:37:21 +080061}
62
lfmylbhf1783a092025-06-07 20:05:22 +080063// 用户发布过的资源
lfmylbhf789e7172025-06-06 18:37:21 +080064interface Resource {
Seamherd6c950f2025-06-09 23:48:28 +080065 resourceId: number;
66 resourceName: string;
67 resourcePicture: string;
68 resourceSummary: string; // 资源简介(一句话)
69 resourceDetail: string; // 资源介绍
70 uploadTime: string; // 上传时间
71 lastUpdateTime: string; // 最近更新时间
72 price: number;
73 downloads: number;
74 likes: number;
75 collections: number;
76 comments: number;
77 seeds: number; // 种子数
78 classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
lfmylbhf789e7172025-06-06 18:37:21 +080079}
80
lfmylbhf1783a092025-06-07 20:05:22 +080081// 用户发布过的资源列表
lfmylbhf789e7172025-06-06 18:37:21 +080082interface ResourceList {
Seamherd6c950f2025-06-09 23:48:28 +080083 records: Resource[];
lfmylbhf789e7172025-06-06 18:37:21 +080084}
85
lfmylbhfa9fa5c82025-06-09 00:34:49 +080086// 帖子
87interface Thread {
Seamherd6c950f2025-06-09 23:48:28 +080088 threadId: number;
89 userId: number;
90 threadPicture: string;
91 title: string;
92 likes: number;
93 createAt: string;
lfmylbhfa9fa5c82025-06-09 00:34:49 +080094}
95
96// 发布帖子列表
97interface ThreadList {
Seamherd6c950f2025-06-09 23:48:28 +080098 records: Thread[];
99 total: number;
100 pages: number;
101 current: number;
102 size: number;
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800103}
104
105// 悬赏
106interface Reward {
Seamherd6c950f2025-06-09 23:48:28 +0800107 rewardId: number;
108 userId: number;
109 rewardPicture: string;
110 rewardName: string;
111 createAt: string;
112 rewardDescription: string;
113 price: number;
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800114}
115
116// 我的悬赏列表
117interface RewardList {
Seamherd6c950f2025-06-09 23:48:28 +0800118 rewardList: Reward[];
119 total: number; // 总记录数
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800120}
121
lfmylbhf1783a092025-06-07 20:05:22 +0800122// 资源标签
123interface GameplayOption {
Seamherd6c950f2025-06-09 23:48:28 +0800124 name: string;
125 code: number;
lfmylbhf1783a092025-06-07 20:05:22 +0800126}
127
128// 资源标签选项
129const gameplayOptions: GameplayOption[] = [
Seamherd6c950f2025-06-09 23:48:28 +0800130 { name: "科技", code: 1 },
131 { name: "魔法", code: 2 },
132 { name: "建筑", code: 3 },
133 { name: "风景", code: 4 },
134 { name: "竞技", code: 5 },
135 { name: "生存", code: 6 },
136 { name: "冒险", code: 7 },
137 { name: "跑酷", code: 8 },
138 { name: "艺术", code: 9 },
139 { name: "剧情", code: 10 },
140 { name: "社交", code: 11 },
141 { name: "策略", code: 12 },
142 { name: "极限", code: 13 },
lfmylbhf1783a092025-06-07 20:05:22 +0800143];
144
lfmylbhf789e7172025-06-06 18:37:21 +0800145export default function UserPage() {
Seamherd6c950f2025-06-09 23:48:28 +0800146 const user = useLocalStorage<User>("user");
147 const userId: number = user?.Id ?? -1;
LaoeGaocid0773912025-06-09 00:38:40 +0800148
Seamherd6c950f2025-06-09 23:48:28 +0800149 // 路由
150 const router = useRouter();
151 // 发布资源列表
152 const [resourceList, setResourceList] = useState<Resource[]>([]);
153 // 用户信息
154 const [userInfo, setUserInfo] = useState<UserInfo>();
155 // 用户数据
156 const [userData, setUserData] = useState<UserData>();
157 // 消息提醒
158 const toast = useRef<Toast>(null);
159 // 资源标签
160 const [selectedGameplay, setSelectedGameplay] = useState<GameplayOption[]>(
161 []
162 );
163 // 资源封面路径
164 const [resourcePictureUrl, setResourcePictureUrl] = useState<string>("");
165 // 主页发布帖子列表
166 const [homePageThread, setHomePageThread] = useState<ThreadList>();
167 // 我的帖子列表
168 const [threadList, setThreadList] = useState<Thread[]>([]);
169 // 我的悬赏列表
170 const [rewardList, setRewardList] = useState<Reward[]>([]);
171 // 控制Tab切换
172 const [activeIndex, setActiveIndex] = useState(0);
173 // 帖子分页
174 const [threadFirst, setThreadFirst] = useState(0);
175 const [threadRows, setThreadRows] = useState(6);
176 const [totalThreads, setTotalThreads] = useState<number>(0);
177 const onThreadPageChange = (event: PaginatorPageChangeEvent) => {
178 setThreadFirst(event.first);
179 setThreadRows(event.rows);
180 };
181 const [torrentUrl, setTorrentUrl] = useState<string>(""); // 上传 .torrent 后得到的 URL
LaoeGaoci68253852025-06-09 22:42:18 +0800182
Seamherd6c950f2025-06-09 23:48:28 +0800183 // 悬赏分页
184 const [rewardFirst, setRewardFirst] = useState(0);
185 const [rewardRows, setRewardRows] = useState(5);
186 const [totalRewards, setTotalRewards] = useState<number>(0);
187 const onRewardPageChange = (event: PaginatorPageChangeEvent) => {
188 setRewardFirst(event.first);
189 setRewardRows(event.rows);
190 };
lfmylbhf789e7172025-06-06 18:37:21 +0800191
Seamherd6c950f2025-06-09 23:48:28 +0800192 useEffect(() => {
193 fetchUserInfo();
194 fetchUserData();
195 fetchResourceList();
196 }, []);
lfmylbhf789e7172025-06-06 18:37:21 +0800197
Seamherd6c950f2025-06-09 23:48:28 +0800198 // 获取用户信息
199 const fetchUserInfo = async () => {
200 try {
201 const response = await axios.get<UserInfo>(
202 process.env.PUBLIC_URL + `/user/info`,
203 {
204 params: { userId },
lfmylbhf789e7172025-06-06 18:37:21 +0800205 }
Seamherd6c950f2025-06-09 23:48:28 +0800206 );
207 console.log("获取用户信息:", response.data);
208 setUserInfo(response.data);
209 } catch (err) {
210 console.error("获取用户信息失败", err);
211 toast.current?.show({
212 severity: "error",
213 summary: "error",
214 detail: "获取用户信息失败",
215 });
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800216 }
Seamherd6c950f2025-06-09 23:48:28 +0800217 };
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800218
Seamherd6c950f2025-06-09 23:48:28 +0800219 // 获取用户数据
220 const fetchUserData = async () => {
221 try {
222 const response = await axios.get<UserData>(
223 process.env.PUBLIC_URL + `/user/data`,
224 {
225 params: { userId },
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800226 }
Seamherd6c950f2025-06-09 23:48:28 +0800227 );
228 console.log("获取用户数据:", response.data);
229 setUserData(response.data);
230 } catch (err) {
231 console.error("获取用户数据失败", err);
232 toast.current?.show({
233 severity: "error",
234 summary: "error",
235 detail: "获取用户数据失败",
236 });
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800237 }
Seamherd6c950f2025-06-09 23:48:28 +0800238 };
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800239
Seamherd6c950f2025-06-09 23:48:28 +0800240 // 格式化数字显示 (3000 -> 3k)
241 const formatCount = (count?: number): string => {
242 if (count == null) return "0"; // 同时处理 undefined/null
243
244 const absCount = Math.abs(count); // 处理负数
245
246 const format = (num: number, suffix: string) => {
247 const fixed = num.toFixed(1);
248 return fixed.endsWith(".0")
249 ? `${Math.floor(num)}${suffix}`
250 : `${fixed}${suffix}`;
251 };
252
253 if (absCount >= 1e6) return format(count / 1e6, "m");
254 if (absCount >= 1e3) return format(count / 1e3, "k");
255 return count.toString();
256 };
257
258 // 获取发布资源
259 const fetchResourceList = async () => {
260 try {
261 const response = await axios.get<ResourceList>(
262 process.env.PUBLIC_URL + `/user/upload`,
263 {
264 params: { userId, pageNumber: 1, rows: 3 },
265 }
266 );
267 console.log("获取发布资源列表:", response.data.records);
268 setResourceList(response.data.records);
269 } catch (err) {
270 console.error("获取发布资源失败", err);
271 toast.current?.show({
272 severity: "error",
273 summary: "error",
274 detail: "获取发布资源失败",
275 });
276 }
277 };
278
279 useEffect(() => {
280 fetchHomePageThread();
281 }, []);
282
283 // 获取主页部分的发布帖子
284 const fetchHomePageThread = async () => {
285 try {
286 const response = await axios.get<ThreadList>(
287 process.env.PUBLIC_URL + `/user/thread`,
288 {
289 params: { userId: userId, pageNumber: 1, rows: 3 },
290 }
291 );
292 console.log("获取主页发布帖子:", response.data);
293 setHomePageThread(response.data);
294 } catch (err) {
295 console.error("获取主页发布帖子失败", err);
296 toast.current?.show({
297 severity: "error",
298 summary: "error",
299 detail: "获取主页发布帖子失败",
300 });
301 }
302 };
303
304 useEffect(() => {
305 fetchThreadList();
306 }, [threadFirst, threadRows]);
307
308 // 获取用户发布的所有帖子
309 const fetchThreadList = async () => {
310 try {
311 const pageNumber = threadFirst / threadRows + 1;
312 const response = await axios.get<ThreadList>(
313 process.env.PUBLIC_URL + `/user/thread`,
314 {
315 params: { userId: userId, pageNumber: pageNumber, rows: threadRows },
316 }
317 );
318 console.log("获取我的帖子:", response.data);
319 setThreadList(response.data.records);
320 setTotalThreads(response.data.total);
321 } catch (err) {
322 console.error("获取我的帖子失败", err);
323 toast.current?.show({
324 severity: "error",
325 summary: "error",
326 detail: "获取我的帖子失败",
327 });
328 }
329 };
330
331 useEffect(() => {
332 fetchRewardList();
333 }, [rewardFirst, rewardRows]);
334
335 // 获取用户发布的所有悬赏
336 const fetchRewardList = async () => {
337 try {
338 const pageNumber = rewardFirst / rewardRows + 1;
339 const response = await axios.get<RewardList>(
340 process.env.PUBLIC_URL + `/user/reward`,
341 {
342 params: { userId: userId, pageNumber: pageNumber, rows: rewardRows },
343 }
344 );
345 console.log("获取我的悬赏:", response.data);
346 setRewardList(response.data.rewardList);
347 setTotalRewards(response.data.total);
348 } catch (err) {
349 console.error("获取我的悬赏失败", err);
350 toast.current?.show({
351 severity: "error",
352 summary: "error",
353 detail: "获取我的悬赏失败",
354 });
355 }
356 };
357
358 // 浮动按钮的子模块
359 const actions = [
360 {
361 template: () => (
362 <Button
363 label="管理资源"
364 onClick={() => router.push(`/user/manage/resources/`)}
365 />
366 ),
367 },
368 {
369 template: () => (
370 <Button
371 label="已购资源"
372 onClick={() => router.push(`/user/purchased-resources/`)}
373 />
374 ),
375 },
376 {
377 template: () => (
378 <Button label="发布资源" onClick={() => setVisible(true)} />
379 ),
380 },
381 {
382 template: () => <Button label="编辑悬赏" />,
383 },
384 ];
385
386 // 发布资源弹窗
387 const [visible, setVisible] = useState(false);
388 const [resourceFormData, setResourceFormData] = useState({
389 resource: {
390 resourceName: "",
391 resourcePicture: "",
392 resourceSummary: "",
393 resourceDetail: "",
394 uploadTime: "",
395 lastUpdateTime: "",
396 price: "",
397 classify: "",
398 },
399 resourceVersionName: "",
400 gameplayList: [""],
401 completeRewardId: null,
402 userId: 0,
403 });
404 const [ingredient, setIngredient] = useState<string>("");
405
406 // 删除悬赏弹窗
407 const [deleteVisible, setDeleteVisible] = useState(false);
408 // 要删除悬赏的id
409 const [deleteRewardId, setDeleteResourceId] = useState<number>(0);
410 // 处理删除悬赏接口
411 const handleDeleteSubmit = async () => {
412 try {
413 // 发送DELETE请求
414 const response = await axios.delete(process.env.PUBLIC_URL + `/reward`, {
415 params: { rewardId: deleteRewardId },
416 });
417 console.log("用户" + userId + "要删除" + deleteRewardId + "号悬赏");
418
419 if (response.status === 204) {
420 console.log("用户成功删除悬赏");
421 toast.current?.show({
422 severity: "success",
423 summary: "Success",
424 detail: "删除悬赏成功",
425 });
426 setDeleteVisible(false);
427 // 重新拉取资源列表
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800428 fetchRewardList();
Seamherd6c950f2025-06-09 23:48:28 +0800429 }
430 } catch (error) {
431 console.error("资源删除失败:", error);
432 toast.current?.show({
433 severity: "error",
434 summary: "error",
435 detail: "密码错误,资源删除失败",
436 });
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800437 }
Seamherd6c950f2025-06-09 23:48:28 +0800438 };
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800439
Seamherd6c950f2025-06-09 23:48:28 +0800440 // 编辑悬赏弹窗
441 const [editVisible, setEditVisible] = useState(false);
442 // 悬赏封面路径
443 const [rewardPictureUrl, setRewardPictureUrl] = useState<string>("");
444 const [editRewardFormData, setEditRewardFormData] = useState({
445 rewardId: 0,
446 rewardName: "",
447 price: "",
448 rewardDescription: "",
449 });
lfmylbhf789e7172025-06-06 18:37:21 +0800450
Seamherd6c950f2025-06-09 23:48:28 +0800451 // 处理编辑资源接口
452 const handleEditSubmit = async () => {
453 try {
454 const postData = {
455 rewardId: editRewardFormData.rewardId,
456 rewardName: editRewardFormData.rewardName,
457 rewardPicture: rewardPictureUrl,
458 price: toNumber(editRewardFormData.price),
459 rewardDescription: editRewardFormData.rewardDescription,
460 };
461 // 发送POST请求
462 const response = await axios.put(
463 process.env.PUBLIC_URL + "/reward/info",
464 postData
465 );
466 console.log("编辑悬赏的信息:", postData);
467
468 if (response.status === 200) {
469 toast.current?.show({
470 severity: "success",
471 summary: "Success",
472 detail: "悬赏编辑成功",
473 });
474 // 编辑成功
475 setEditVisible(false);
476 fetchRewardList();
477 }
478 } catch (error) {
479 console.error("悬赏编辑失败:", error);
480 toast.current?.show({
481 severity: "error",
482 summary: "error",
483 detail: "悬赏编辑失败",
484 });
485 }
486 };
487
488 // 上传资源接口
489 const handleSubmit = async () => {
490 try {
491 // 规定用户必须输入的内容
492 if (resourceFormData.resource.resourceName == "") {
493 toast.current?.show({
494 severity: "info",
495 summary: "error",
496 detail: "缺少资源名称",
497 });
498 return;
499 }
500 if (resourceFormData.resource.resourceSummary == "") {
501 toast.current?.show({
502 severity: "info",
503 summary: "error",
504 detail: "缺少资源简介",
505 });
506 return;
507 }
508 if (resourceFormData.resourceVersionName == "") {
509 toast.current?.show({
510 severity: "info",
511 summary: "error",
512 detail: "缺少资源版本名",
513 });
514 return;
515 }
516 if (resourceFormData.resource.price == "") {
517 toast.current?.show({
518 severity: "info",
519 summary: "error",
520 detail: "缺少资源价格",
521 });
522 return;
523 }
524 if (resourceFormData.resource.classify == "") {
525 toast.current?.show({
526 severity: "info",
527 summary: "error",
528 detail: "缺少资源分类",
529 });
530 return;
531 }
532 if (
533 resourceFormData.gameplayList.length === 1 &&
534 resourceFormData.gameplayList[0] === ""
535 ) {
536 toast.current?.show({
537 severity: "info",
538 summary: "error",
539 detail: "缺少资源标签",
540 });
541 return;
542 }
543 if (resourcePictureUrl === "") {
544 toast.current?.show({
545 severity: "info",
546 summary: "error",
547 detail: "缺少资源封面",
548 });
549 return;
550 }
551
552 const currentDate = new Date().toISOString().split("T")[0];
553 const postData = {
lfmylbhf1783a092025-06-07 20:05:22 +0800554 resource: {
Seamherd6c950f2025-06-09 23:48:28 +0800555 resourceName: resourceFormData.resource.resourceName,
556 resourcePicture: resourcePictureUrl,
557 resourceSummary: resourceFormData.resource.resourceSummary,
558 resourceDetail: resourceFormData.resource.resourceDetail,
559 uploadTime: currentDate,
560 lastUpdateTime: currentDate,
561 price: toNumber(resourceFormData.resource.price),
562 classify: resourceFormData.resource.classify,
lfmylbhf1783a092025-06-07 20:05:22 +0800563 },
Seamherd6c950f2025-06-09 23:48:28 +0800564 gameplayList: resourceFormData.gameplayList,
565 resourceVersionName: resourceFormData.resourceVersionName,
lfmylbhf1783a092025-06-07 20:05:22 +0800566 completeRewardId: null,
Seamherd6c950f2025-06-09 23:48:28 +0800567 userId, // 记得用户登录状态获取
568 };
569 // 发送POST请求
570 const response = await axios.post(
571 process.env.PUBLIC_URL + "/resource",
572 postData
573 );
574 console.log("上传资源的信息:", postData);
lfmylbhf1783a092025-06-07 20:05:22 +0800575
Seamherd6c950f2025-06-09 23:48:28 +0800576 if (response.status === 200) {
577 toast.current?.show({
578 severity: "success",
579 summary: "Success",
580 detail: "资源上传成功",
581 });
582 console.log(torrentUrl);
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800583 try {
Seamherd6c950f2025-06-09 23:48:28 +0800584 const versionResponse = await axios.post(
585 `${process.env.PUBLIC_URL}/resource/version`,
586 {
587 resourceVersionName: postData.resourceVersionName,
588 resourceId: Number(response.data),
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800589 }
Seamherd6c950f2025-06-09 23:48:28 +0800590 );
591 // 上传资源文件
592 const btPayload = {
593 torrentUrl: torrentUrl,
594 infoHash: "8",
595 uploadTime: currentDate,
596 uploaderUserId: userId,
597 resourceVersionId: Number(versionResponse.data),
598 };
599
600 await axios.post(`${process.env.PUBLIC_URL}/file/bt`, btPayload);
601 toast.current?.show({
602 severity: "success",
603 summary: "资源登记成功",
604 detail: "已同步种子信息",
605 });
606 } catch (btError) {
607 console.error("种子登记失败:", btError);
608 toast.current?.show({
609 severity: "warn",
610 summary: "资源已上传",
611 detail: "但种子登记失败",
612 });
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800613 }
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800614
Seamherd6c950f2025-06-09 23:48:28 +0800615 // 上传成功
616 setVisible(false);
617 // 重置表单
618 setResourceFormData({
619 resource: {
620 resourceName: "",
621 resourcePicture: "",
622 resourceSummary: "",
623 resourceDetail: "",
624 uploadTime: "",
625 lastUpdateTime: "",
626 price: "",
627 classify: "",
628 },
629 resourceVersionName: "",
630 gameplayList: [],
631 completeRewardId: null,
632 userId: 0,
633 });
634 // 重置资源分类
635 setIngredient("");
636 // 重置资源标签
637 setSelectedGameplay([]);
638 // 重置资源封面
639 setResourcePictureUrl("");
640 setTorrentUrl("");
641 }
642 } catch (error) {
643 console.error("资源上传失败:", error);
644 toast.current?.show({
645 severity: "error",
646 summary: "error",
647 detail: "资源上传失败",
648 });
649 }
650 };
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800651
Seamherd6c950f2025-06-09 23:48:28 +0800652 return (
653 <div className="user-container">
654 <Toast ref={toast}></Toast>
655 {/*个人信息*/}
656 <div className="user-profile-card">
657 <Avatar
658 image={userInfo?.avatar}
659 className="user-avatar"
660 shape="circle"
661 />
662 <div className="user-info">
663 <div className="user-detail-info">
664 <div className="name-container">
665 <h2 className="name">{userInfo?.username}</h2>
666 <span className="signature">{userInfo?.signature}</span>
667 </div>
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800668
Seamherd6c950f2025-06-09 23:48:28 +0800669 <div className="stats-container">
670 <div className="stats">
671 <span className="stats-label">粉丝:</span>
672 <span className="stats-value">{userInfo?.followerCount}</span>
673 </div>
674 <div className="stats">
675 <span className="stats-label">累计上传量:</span>
676 <span className="stats-value">
677 {formatCount(userData?.uploadAmount)}
678 </span>
679 </div>
680 <div className="stats">
681 <span className="stats-label">关注:</span>
682 <span className="stats-value">{userInfo?.subscriberCount}</span>
683 </div>
684 <div className="stats">
685 <span className="stats-label">累计被下载量:</span>
686 <span className="stats-value">
687 {formatCount(userData?.beDownloadedAmount)}
688 </span>
689 </div>
690 </div>
691 </div>
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800692
Seamherd6c950f2025-06-09 23:48:28 +0800693 <Button label="关注" className="action-button" />
694 </div>
695 </div>
696
697 {/*个人内容*/}
698 <TabView
699 activeIndex={activeIndex}
700 onTabChange={(e) => setActiveIndex(e.index)}
701 >
702 <TabPanel header="主页">
703 {/*发布资源*/}
704 <div className="homepage-item">
705 <div className="section-header">
706 <h1>发布资源</h1>
707 <Button
708 label="显示更多"
709 link
710 onClick={() => router.push("/user/manage/resources/")}
711 />
712 </div>
713 <div className="resource-grid">
714 {resourceList.map((resourceList) => (
715 <Card
716 key={resourceList.resourceId}
717 className="resource-card"
718 onClick={() =>
719 router.push(
720 `/resource/resource-detail/${resourceList.resourceId}`
721 )
722 }
723 >
724 <Image
725 src={resourceList.resourcePicture}
726 alt={resourceList.resourceName}
727 width="368"
728 height="200"
729 />
730 <div className="card-content">
731 <h3>{resourceList.resourceName}</h3>
732 <div className="view-count">
733 <Fire theme="outline" size="16" fill="#FF8D1A" />
734 <span>{resourceList.likes}</span>
735 </div>
736 </div>
737 </Card>
738 ))}
739 </div>
740 </div>
741
742 {/*发布帖子*/}
743 <div className="homepage-item">
744 <div className="section-header">
745 <h1>发布帖子</h1>
746 <Button label="显示更多" link onClick={() => setActiveIndex(1)} />
747 </div>
748 <div className="resource-grid">
749 {homePageThread?.records.map((homePageThread) => (
750 <Card
751 key={homePageThread.threadId}
752 className="resource-card"
753 onClick={() =>
754 router.push(
755 `/community/thread-detail/${homePageThread.threadId}`
756 )
757 }
758 >
759 <Image
760 src={homePageThread.threadPicture}
761 alt={homePageThread.title}
762 width="368"
763 height="200"
764 />
765 <div className="card-content">
766 <h3>{homePageThread.title}</h3>
767 <div className="view-count">
768 <Fire theme="outline" size="16" fill="#FF8D1A" />
769 <span>{homePageThread.likes}</span>
770 </div>
771 </div>
772 </Card>
773 ))}
774 </div>
775 </div>
776 </TabPanel>
777
778 <TabPanel header="帖子">
779 {/*我的帖子*/}
780 <div className="homepage-item">
781 <div className="section-header">
782 <h1>我的帖子</h1>
783 </div>
784 <div className="resource-grid">
785 {threadList.map((threadList) => (
786 <Card
787 key={threadList.threadId}
788 className="resource-card"
789 onClick={() =>
790 router.push(
791 `/community/thread-detail/${threadList.threadId}`
792 )
793 }
794 >
795 <Image
796 src={threadList.threadPicture}
797 alt={threadList.title}
798 width="368"
799 height="200"
800 />
801 <div className="card-content">
802 <h3>{threadList.title}</h3>
803 <div className="view-count">
804 <Fire theme="outline" size="16" fill="#FF8D1A" />
805 <span>{threadList.likes}</span>
806 </div>
807 </div>
808 </Card>
809 ))}
810 </div>
811 {totalThreads > 6 && (
812 <Paginator
813 className="Paginator"
814 first={threadFirst}
815 rows={threadRows}
816 totalRecords={totalThreads}
817 rowsPerPageOptions={[6, 12]}
818 onPageChange={onThreadPageChange}
819 />
820 )}
821 </div>
822 </TabPanel>
823
824 <TabPanel header="收藏"></TabPanel>
825
826 <TabPanel header="数据"></TabPanel>
827
828 <TabPanel header="悬赏">
829 <div className="section-header">
830 <h1>我的悬赏</h1>
831 </div>
832
833 <div className="resource-list">
834 {rewardList.map((rewardItem) => (
835 <Card
836 key={rewardItem.rewardId}
837 className="resources-list-card"
838 onClick={() =>
839 router.push(`/reward/reward-detail/${rewardItem.rewardId}`)
840 }
841 >
842 <Image
843 alt="avatar"
844 src={"rewards/" + rewardItem.rewardPicture}
845 className="resource-avatar"
846 width="250"
847 height="140"
848 />
849 <div className="resource-header">
850 <div className="resource-content">
851 <h3>{rewardItem.rewardName}</h3>
852 </div>
853
854 <div className="resource-operation">
855 <Button
856 label="编辑"
857 onClick={(e) => {
858 e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
859 setEditVisible(true);
860
861 setEditRewardFormData({
862 rewardId: rewardItem.rewardId,
863 rewardName: rewardItem.rewardName,
864 price: rewardItem.price.toString(),
865 rewardDescription: rewardItem.rewardDescription,
866 });
867 console.log("用户编辑悬赏");
868 }}
869 />
870 <Button
871 label="删除"
872 onClick={(e) => {
873 e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
874 setDeleteResourceId(rewardItem.rewardId);
875 setDeleteVisible(true);
876 }}
877 style={{ backgroundColor: "rgba(255, 87, 51, 1)" }}
878 />
879 </div>
880 </div>
881 </Card>
882 ))}
883
884 {totalRewards > 5 && (
885 <Paginator
886 className="Paginator"
887 first={rewardFirst}
888 rows={rewardRows}
889 totalRecords={totalRewards}
890 rowsPerPageOptions={[5, 10]}
891 onPageChange={onRewardPageChange}
892 />
893 )}
894 </div>
895 </TabPanel>
896 </TabView>
897
898 {/*浮动按钮*/}
899 <div className="card">
900 <SpeedDial
901 model={actions}
902 direction="up"
903 style={{ position: "fixed", bottom: "2rem", right: "2rem" }}
904 showIcon="pi pi-plus"
905 hideIcon="pi pi-times"
906 buttonClassName="custom-speeddial-button"
907 />
908 </div>
909
910 {/*发布资源弹窗*/}
911 <Dialog
912 header="发布资源"
913 visible={visible}
914 onHide={() => setVisible(false)}
915 className="publish-dialog"
916 modal
917 footer={
918 <div className="dialog-footer">
919 <Button
920 label="发布"
921 icon="pi pi-check"
922 onClick={handleSubmit}
923 autoFocus
924 />
925 <Button
926 label="取消"
927 icon="pi pi-times"
928 onClick={() => setVisible(false)}
929 className="p-button-text"
930 />
931 </div>
lfmylbhfa9fa5c82025-06-09 00:34:49 +0800932 }
Seamherd6c950f2025-06-09 23:48:28 +0800933 >
934 <div className="publish-form">
935 <div className="form-field">
936 <div className="form-field-header">
937 <span className="form-field-sign">*</span>
938 <label htmlFor="name">资源名称</label>
939 </div>
940 <InputText
941 id="name"
942 value={resourceFormData.resource.resourceName}
943 onChange={(e) =>
944 setResourceFormData((prev) => ({
945 ...prev, // 复制顶层所有属性
946 resource: {
947 ...prev.resource, // 复制resource对象的所有属性
948 resourceName: e.target.value, // 只更新resourceName
949 },
950 }))
951 }
952 placeholder="请输入资源名称"
953 className="w-full"
954 />
955 </div>
956 <div className="form-field">
957 <div className="form-field-header">
958 <span className="form-field-sign">*</span>
959 <label htmlFor="summary">资源简介</label>
960 </div>
961 <InputText
962 id="summary"
963 value={resourceFormData.resource.resourceSummary}
964 onChange={(e) =>
965 setResourceFormData((prev) => ({
966 ...prev, // 复制顶层所有属性
967 resource: {
968 ...prev.resource, // 复制resource对象的所有属性
969 resourceSummary: e.target.value,
970 },
971 }))
972 }
973 placeholder="请输入资源简介(一句话)"
974 className="w-full"
975 />
976 </div>
977 <div className="form-field">
978 <div className="form-field-header">
979 <span className="form-field-sign">*</span>
980 <label htmlFor="summary">资源版本名</label>
981 </div>
982 <InputText
983 id="summary"
984 value={resourceFormData.resourceVersionName}
985 onChange={(e) =>
986 setResourceFormData((prev) => ({
987 ...prev, // 复制顶层所有属性
988 resourceVersionName: e.target.value,
989 }))
990 }
991 placeholder="请输入资源版本名"
992 className="w-full"
993 />
994 </div>
995 <div className="form-field">
996 <div className="form-field-header">
997 <label htmlFor="detail">资源介绍</label>
998 </div>
999 <InputTextarea
1000 id="detail"
1001 value={resourceFormData.resource.resourceDetail}
1002 onChange={(e) =>
1003 setResourceFormData((prev) => ({
1004 ...prev, // 复制顶层所有属性
1005 resource: {
1006 ...prev.resource, // 复制resource对象的所有属性
1007 resourceDetail: e.target.value,
1008 },
1009 }))
1010 }
1011 rows={5}
1012 placeholder="请输入资源介绍"
1013 className="w-full"
1014 />
1015 </div>
1016 <div className="form-field">
1017 <div className="form-field-header">
1018 <span className="form-field-sign">*</span>
1019 <label htmlFor="price">价格</label>
1020 </div>
1021 <InputText
1022 id="price"
1023 value={resourceFormData.resource.price}
1024 onChange={(e) =>
1025 setResourceFormData((prev) => ({
1026 ...prev, // 复制顶层所有属性
1027 resource: {
1028 ...prev.resource, // 复制resource对象的所有属性
1029 price: e.target.value,
1030 },
1031 }))
1032 }
1033 placeholder="请输入资源价格"
1034 className="w-full"
1035 />
1036 </div>
1037 <div className="form-field">
1038 <div className="form-field-header">
1039 <span className="form-field-sign">*</span>
1040 <label htmlFor="classify">资源分类(请选择一项)</label>
1041 </div>
1042 <div className="form-field-classify">
1043 <div className="flex align-items-center">
1044 <RadioButton
1045 inputId="ingredient1"
1046 name="pizza"
1047 value="resourcePack"
1048 onChange={(e: RadioButtonChangeEvent) => {
1049 setResourceFormData((prev) => ({
1050 ...prev, // 复制顶层所有属性
1051 resource: {
1052 ...prev.resource, // 复制resource对象的所有属性
1053 classify: e.target.value,
1054 },
1055 }));
1056 setIngredient(e.value);
1057 console.log(ingredient);
1058 // console.log(resourceFormData.resource.classify);
1059 }}
1060 checked={ingredient === "resourcePack"}
1061 />
1062 <label htmlFor="ingredient1" className="ml-2">
1063 材质包
1064 </label>
1065 </div>
1066 <div className="flex align-items-center">
1067 <RadioButton
1068 inputId="ingredient2"
1069 name="pizza"
1070 value="modPack"
1071 onChange={(e: RadioButtonChangeEvent) => {
1072 setResourceFormData((prev) => ({
1073 ...prev, // 复制顶层所有属性
1074 resource: {
1075 ...prev.resource, // 复制resource对象的所有属性
1076 classify: e.target.value,
1077 },
1078 }));
1079 setIngredient(e.value);
1080 }}
1081 checked={ingredient === "modPack"}
1082 />
1083 <label htmlFor="ingredient2" className="ml-2">
1084 整合包
1085 </label>
1086 </div>
1087 <div className="flex align-items-center">
1088 <RadioButton
1089 inputId="ingredient3"
1090 name="pizza"
1091 value="mod"
1092 onChange={(e: RadioButtonChangeEvent) => {
1093 setResourceFormData((prev) => ({
1094 ...prev, // 复制顶层所有属性
1095 resource: {
1096 ...prev.resource, // 复制resource对象的所有属性
1097 classify: e.target.value,
1098 },
1099 }));
1100 setIngredient(e.value);
1101 }}
1102 checked={ingredient === "mod"}
1103 />
1104 <label htmlFor="ingredient3" className="ml-2">
1105 模组
1106 </label>
1107 </div>
1108 <div className="flex align-items-center">
1109 <RadioButton
1110 inputId="ingredient4"
1111 name="pizza"
1112 value="map"
1113 onChange={(e: RadioButtonChangeEvent) => {
1114 setResourceFormData((prev) => ({
1115 ...prev, // 复制顶层所有属性
1116 resource: {
1117 ...prev.resource, // 复制resource对象的所有属性
1118 classify: e.target.value,
1119 },
1120 }));
1121 setIngredient(e.value);
1122 }}
1123 checked={ingredient === "map"}
1124 />
1125 <label htmlFor="ingredient4" className="ml-2">
1126 地图
1127 </label>
1128 </div>
1129 </div>
1130 </div>
1131 <div className="form-field">
1132 <div className="form-field-header">
1133 <span className="form-field-sign">*</span>
1134 <label htmlFor="gameplayList">资源标签</label>
1135 </div>
1136 <MultiSelect
1137 value={selectedGameplay}
1138 onChange={(e: MultiSelectChangeEvent) => {
1139 const selectedOptions = e.value as GameplayOption[];
1140 // 提取选中项的 name 属性组成字符串数组
1141 const selectedNames = selectedOptions.map((item) => item.name);
lfmylbhf1783a092025-06-07 20:05:22 +08001142
Seamherd6c950f2025-06-09 23:48:28 +08001143 setResourceFormData((prev) => ({
1144 ...prev,
1145 gameplayList: selectedNames,
1146 }));
1147 setSelectedGameplay(selectedOptions);
1148 }}
1149 options={gameplayOptions}
1150 display="chip"
1151 optionLabel="name"
1152 placeholder="请选择资源标签"
1153 className="w-full md:w-20rem"
1154 />
1155 </div>
1156 <div className="form-field">
1157 <div className="form-field-header">
1158 <span className="form-field-sign">*</span>
1159 <label>封面图片</label>
1160 </div>
1161 <FileUpload
1162 mode="advanced"
1163 name="resource-image"
1164 customUpload
1165 uploadHandler={async (e) => {
1166 const formData = new FormData();
1167 formData.append("file", e.files[0]);
LaoeGaoci68253852025-06-09 22:42:18 +08001168
1169 try {
Seamherd6c950f2025-06-09 23:48:28 +08001170 const res = await axios.post(
1171 `${process.env.PUBLIC_URL}/file`,
1172 formData
1173 );
1174
1175 const fileUrl = res.data.url;
1176 console.log(fileUrl);
1177 setResourcePictureUrl(fileUrl);
1178 toast.current?.show({
1179 severity: "success",
1180 summary: "上传成功",
1181 });
1182 } catch (error) {
1183 console.log(error);
1184 toast.current?.show({
1185 severity: "error",
1186 summary: "上传失败",
1187 });
LaoeGaoci68253852025-06-09 22:42:18 +08001188 }
Seamherd6c950f2025-06-09 23:48:28 +08001189 }}
1190 auto
1191 chooseLabel="上传资源封面"
1192 />
1193 </div>
1194 <div className="form-field">
1195 <div className="form-field-header">
1196 <span className="form-field-sign">*</span>
1197 <label>上传资源文件</label>
lfmylbhf789e7172025-06-06 18:37:21 +08001198 </div>
Seamherd6c950f2025-06-09 23:48:28 +08001199 <FileUpload
1200 mode="advanced"
1201 name="resource-file"
1202 customUpload
1203 uploadHandler={async (e) => {
1204 const formData = new FormData();
1205 formData.append("file", e.files[0]);
lfmylbhf789e7172025-06-06 18:37:21 +08001206
Seamherd6c950f2025-06-09 23:48:28 +08001207 try {
1208 const res = await axios.post(
1209 `${process.env.PUBLIC_URL}/file`,
1210 formData
1211 );
1212 const fileUrl = res.data;
1213 console.log("上传的 torrent 文件 URL:", fileUrl);
1214 setTorrentUrl(fileUrl);
1215 toast.current?.show({
1216 severity: "success",
1217 summary: "上传成功",
1218 detail: "资源文件已上传",
1219 });
1220 } catch (error) {
1221 console.error("上传资源文件失败:", error);
1222 toast.current?.show({
1223 severity: "error",
1224 summary: "上传失败",
1225 detail: "资源文件上传失败",
1226 });
lfmylbhf1783a092025-06-07 20:05:22 +08001227 }
Seamherd6c950f2025-06-09 23:48:28 +08001228 }}
1229 auto
1230 chooseLabel="上传资源文件(.torrent)"
1231 />
1232 </div>
lfmylbhf789e7172025-06-06 18:37:21 +08001233 </div>
Seamherd6c950f2025-06-09 23:48:28 +08001234 </Dialog>
lfmylbhf789e7172025-06-06 18:37:21 +08001235
Seamherd6c950f2025-06-09 23:48:28 +08001236 {/*删除悬赏弹窗*/}
1237 <Dialog
1238 header="删除悬赏"
1239 visible={deleteVisible}
1240 onHide={() => setDeleteVisible(false)}
1241 className="resource-delete-dialog"
1242 modal
1243 footer={
1244 <div className="dialog-footer">
1245 <Button
1246 label="确认"
1247 icon="pi pi-check"
1248 onClick={handleDeleteSubmit}
1249 autoFocus
1250 />
1251 <Button
1252 label="取消"
1253 icon="pi pi-times"
1254 onClick={() => setDeleteVisible(false)}
1255 className="p-button-text"
1256 />
1257 </div>
1258 }
1259 >
1260 <div className="dialog-form">
1261 <span style={{ marginBottom: "10px" }}>确认是否删除该悬赏?</span>
1262 </div>
1263 </Dialog>
1264
1265 {/*编辑资源弹窗*/}
1266 <Dialog
1267 header="编辑资源"
1268 visible={editVisible}
1269 onHide={() => setEditVisible(false)}
1270 className="resource-edit-dialog"
1271 modal
1272 footer={
1273 <div className="dialog-footer">
1274 <Button
1275 label="确认"
1276 icon="pi pi-check"
1277 onClick={handleEditSubmit}
1278 autoFocus
1279 />
1280 <Button
1281 label="取消"
1282 icon="pi pi-times"
1283 onClick={() => setEditVisible(false)}
1284 className="p-button-text"
1285 />
1286 </div>
1287 }
1288 >
1289 <div className="dialog-form">
1290 <div className="form-field">
1291 <div className="form-field-header">
1292 <label htmlFor="name">更改标题</label>
1293 </div>
1294 <InputText
1295 id="name"
1296 value={editRewardFormData.rewardName}
1297 onChange={(e) =>
1298 setEditRewardFormData((prev) => ({
1299 ...prev,
1300 rewardName: e.target.value,
1301 }))
1302 }
1303 className="w-full"
1304 />
1305 </div>
1306 <div className="form-field">
1307 <div className="form-field-header">
1308 <label htmlFor="price">更改定价</label>
1309 </div>
1310 <InputText
1311 id="price"
1312 value={editRewardFormData.price}
1313 onChange={(e) =>
1314 setEditRewardFormData((prev) => ({
1315 ...prev,
1316 price: e.target.value,
1317 }))
1318 }
1319 className="w-full"
1320 />
1321 </div>
1322 <div className="form-field">
1323 <div className="form-field-header">
1324 <label htmlFor="description">更改需求</label>
1325 </div>
1326 <InputTextarea
1327 id="description"
1328 value={editRewardFormData.rewardDescription}
1329 onChange={(e) =>
1330 setEditRewardFormData((prev) => ({
1331 ...prev,
1332 rewardDescription: e.target.value,
1333 }))
1334 }
1335 rows={5}
1336 className="w-full"
1337 />
1338 </div>
1339 <div className="form-field">
1340 <div className="form-field-header">
1341 <span className="form-field-sign">*</span>
1342 <label>封面图片</label>
1343 </div>
1344 <FileUpload
1345 mode="advanced"
1346 name="reward-image"
1347 customUpload
1348 uploadHandler={async (e) => {
1349 const formData = new FormData();
1350 formData.append("file", e.files[0]);
1351
1352 try {
1353 const res = await axios.post(
1354 `${process.env.PUBLIC_URL}/file`,
1355 formData
1356 );
1357
1358 const fileUrl = res.data.url;
1359 console.log(fileUrl);
1360 setRewardPictureUrl(fileUrl);
1361 toast.current?.show({
1362 severity: "success",
1363 summary: "上传成功",
1364 });
1365 } catch (error) {
1366 console.log(error);
1367 toast.current?.show({
1368 severity: "error",
1369 summary: "上传失败",
1370 });
1371 }
1372 }}
1373 auto
1374 accept="image/*"
1375 chooseLabel="选择悬赏封面"
1376 />
1377 </div>
1378 </div>
1379 </Dialog>
1380 </div>
1381 );
1382}