From 891b3d4a1a0739e2153c98e753488dcbff3f005b Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 13 Jul 2025 16:41:39 +0800 Subject: [PATCH] Add user files. --- frontend/src/network/models.ts | 1 + frontend/src/network/network.ts | 14 +++ frontend/src/pages/activities_page.tsx | 2 +- frontend/src/pages/user_page.tsx | 142 +++++++++++++++++++++++-- server/api/file.go | 20 ++++ server/dao/file.go | 17 +++ server/model/file.go | 31 ++++-- server/service/file.go | 23 ++++ 8 files changed, 236 insertions(+), 14 deletions(-) diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index 06c0cfb..233b8d2 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -103,6 +103,7 @@ export interface RFile { size: number; is_redirect: boolean; user: User; + resource?: Resource; } export interface UploadingFile { diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 46c2224..3c78537 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -574,6 +574,20 @@ class Network { ); } + async getUserFiles( + username: string, + page: number = 1, + ): Promise> { + return this._callApi(() => + axios.get( + `${this.apiBaseUrl}/files/user/${encodeURIComponent(username)}`, + { + params: { page }, + }, + ), + ); + } + getFileDownloadLink(fileId: string, cfToken: string): string { return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`; } diff --git a/frontend/src/pages/activities_page.tsx b/frontend/src/pages/activities_page.tsx index c8d2645..c7c5d76 100644 --- a/frontend/src/pages/activities_page.tsx +++ b/frontend/src/pages/activities_page.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import Loading from "../components/loading.tsx"; import { CommentContent } from "../components/comment_tile.tsx"; -import {MdOutlineArchive, MdOutlinePhotoAlbum} from "react-icons/md"; +import { MdOutlineArchive, MdOutlinePhotoAlbum } from "react-icons/md"; import Badge from "../components/badge.tsx"; export default function ActivitiesPage() { diff --git a/frontend/src/pages/user_page.tsx b/frontend/src/pages/user_page.tsx index e892895..0959b35 100644 --- a/frontend/src/pages/user_page.tsx +++ b/frontend/src/pages/user_page.tsx @@ -1,8 +1,8 @@ import { useParams, useLocation, useNavigate } from "react-router"; -import { CommentWithResource, User } from "../network/models"; +import { CommentWithResource, RFile, User } from "../network/models"; import { network } from "../network/network"; import showToast from "../components/toast"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import ResourcesView from "../components/resources_view"; import Loading from "../components/loading"; import Pagination from "../components/pagination"; @@ -26,23 +26,27 @@ export default function UserPage() { const username = rawUsername ? decodeURIComponent(rawUsername) : ""; // 从 hash 中获取当前页面,默认为 resources - const getPageFromHash = () => { + const getPageFromHash = useCallback(() => { const hash = location.hash.slice(1); // 移除 # 号 if (hash === "comments") return 1; + if (hash === "files") return 2; return 0; // 默认为 resources - }; + }, [location.hash]); const [page, setPage] = useState(getPageFromHash()); // 监听 hash 变化 useEffect(() => { setPage(getPageFromHash()); - }, [location.hash]); + }, [location.hash, getPageFromHash]); // 更新 hash 的函数 const updateHash = (newPage: number) => { - const hash = newPage === 1 ? "#comments" : "#resources"; - navigate(location.pathname + hash, { replace: true }); + const hashs = ["resources", "comments", "files"]; + const newHash = hashs[newPage] || "resources"; + if (location.hash.slice(1) !== newHash) { + navigate(`/user/${username}#${newHash}`, { replace: true }); + } }; useEffect(() => { @@ -91,10 +95,18 @@ export default function UserPage() { > Comments +
updateHash(2)} + > + Files +
{page === 0 && } {page === 1 && } + {page === 2 && }
@@ -228,3 +240,119 @@ function CommentsList({ ); } + +function UserFiles({ user }: { user: User }) { + const [page, setPage] = useState(1); + + const [maxPage, setMaxPage] = useState(0); + + return ( +
+ + {maxPage ? ( +
+ +
+ ) : null} +
+ ); +} + +function fileSizeToString(size: number) { + if (size < 1024) { + return size + "B"; + } else if (size < 1024 * 1024) { + return (size / 1024).toFixed(2) + "KB"; + } else if (size < 1024 * 1024 * 1024) { + return (size / 1024 / 1024).toFixed(2) + "MB"; + } else { + return (size / 1024 / 1024 / 1024).toFixed(2) + "GB"; + } +} + +function FilesList({ + username, + page, + maxPageCallback, +}: { + username: string; + page: number; + maxPageCallback: (maxPage: number) => void; +}) { + const [files, setFiles] = useState(null); + + const navigate = useNavigate(); + + const { t } = useTranslation(); + + useEffect(() => { + network.getUserFiles(username, page).then((res) => { + if (res.success) { + setFiles(res.data!); + maxPageCallback(res.totalPages || 1); + } else { + showToast({ + message: res.message, + type: "error", + }); + } + }); + }, [maxPageCallback, page, username]); + + if (files == null) { + return ( +
+ +
+ ); + } + + return ( + <> + {files.map((file) => { + return ( + { + e.preventDefault(); + navigate(`/resources/${file.resource!.id}#files`); + }} + > +
+

{file!.filename}

+

+ {file!.description} +

+

+ + + {file!.is_redirect + ? t("Redirect") + : fileSizeToString(file!.size)} + + + + {(() => { + let title = file.resource!.title; + if (title.length > 20) { + title = title.slice(0, 20) + "..."; + } + return title; + })()} + +

+
+
+ ); + })} + + ); +} diff --git a/server/api/file.go b/server/api/file.go index 8d03e91..9073ad0 100644 --- a/server/api/file.go +++ b/server/api/file.go @@ -30,6 +30,7 @@ func AddFileRoutes(router fiber.Router) { fileGroup.Delete("/:id", deleteFile) fileGroup.Get("/download/local", downloadLocalFile) fileGroup.Get("/download/:id", downloadFile, middleware.NewDynamicRequestLimiter(config.MaxDownloadsPerDayForSingleIP, 24*time.Hour)) + fileGroup.Get("/user/:username", listUserFiles, middleware.NewRequestLimiter(100, 24*time.Hour)) } } @@ -273,3 +274,22 @@ func createServerDownloadTask(c fiber.Ctx) error { Data: result, }) } + +func listUserFiles(c fiber.Ctx) error { + username := c.Params("username") + page, err := strconv.Atoi(c.Query("page", "1")) + if err != nil || page < 1 { + return model.NewRequestError("Invalid page number") + } + + files, totalPages, err := service.ListUserFiles(username, page) + if err != nil { + return err + } + + return c.JSON(model.PageResponse[*model.FileView]{ + Success: true, + Data: files, + TotalPages: totalPages, + }) +} diff --git a/server/dao/file.go b/server/dao/file.go index 6b59cbb..c117c89 100644 --- a/server/dao/file.go +++ b/server/dao/file.go @@ -215,3 +215,20 @@ func SetFileStorageKeyAndSize(id string, storageKey string, size int64) error { } return nil } + +func ListUserFiles(userID uint, page, pageSize int) ([]*model.File, int64, error) { + var files []*model.File + var count int64 + + if err := db.Model(&model.File{}). + Preload("Resource"). + Where("user_id = ?", userID). + Count(&count). + Order("created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&files).Error; err != nil { + return nil, 0, err + } + return files, count, nil +} diff --git a/server/model/file.go b/server/model/file.go index e4c5d8f..9dc0ab4 100644 --- a/server/model/file.go +++ b/server/model/file.go @@ -21,12 +21,13 @@ type File struct { } type FileView struct { - ID string `json:"id"` - Filename string `json:"filename"` - Description string `json:"description"` - Size int64 `json:"size"` - IsRedirect bool `json:"is_redirect"` - User UserView `json:"user"` + ID string `json:"id"` + Filename string `json:"filename"` + Description string `json:"description"` + Size int64 `json:"size"` + IsRedirect bool `json:"is_redirect"` + User UserView `json:"user"` + Resource *ResourceView `json:"resource,omitempty"` } func (f *File) ToView() *FileView { @@ -39,3 +40,21 @@ func (f *File) ToView() *FileView { User: f.User.ToView(), } } + +func (f *File) ToViewWithResource() *FileView { + var resource *ResourceView + if f.Resource.ID != 0 { + rv := f.Resource.ToView() + resource = &rv + } + + return &FileView{ + ID: f.UUID, + Filename: f.Filename, + Description: f.Description, + Size: f.Size, + IsRedirect: f.RedirectUrl != "", + User: f.User.ToView(), + Resource: resource, + } +} diff --git a/server/service/file.go b/server/service/file.go index a6b0cb7..108822b 100644 --- a/server/service/file.go +++ b/server/service/file.go @@ -614,3 +614,26 @@ func CreateServerDownloadTask(uid uint, url, filename, description string, resou return file.ToView(), nil } + +func ListUserFiles(username string, page int) ([]*model.FileView, int, error) { + user, err := dao.GetUserByUsername(username) + if err != nil { + log.Error("failed to get user by username: ", err) + return nil, 0, model.NewNotFoundError("user not found") + } + uid := user.ID + files, total, err := dao.ListUserFiles(uid, page, pageSize) + if err != nil { + log.Error("failed to list user files: ", err) + return nil, 0, model.NewInternalServerError("failed to list user files") + } + + fileViews := make([]*model.FileView, len(files)) + for i, file := range files { + fileViews[i] = file.ToViewWithResource() + } + + totalPages := (total + pageSize - 1) / pageSize + + return fileViews, int(totalPages), nil +}