mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add user files.
This commit is contained in:
@@ -103,6 +103,7 @@ export interface RFile {
|
|||||||
size: number;
|
size: number;
|
||||||
is_redirect: boolean;
|
is_redirect: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
|
resource?: Resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadingFile {
|
export interface UploadingFile {
|
||||||
|
@@ -574,6 +574,20 @@ class Network {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserFiles(
|
||||||
|
username: string,
|
||||||
|
page: number = 1,
|
||||||
|
): Promise<PageResponse<RFile>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(
|
||||||
|
`${this.apiBaseUrl}/files/user/${encodeURIComponent(username)}`,
|
||||||
|
{
|
||||||
|
params: { page },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getFileDownloadLink(fileId: string, cfToken: string): string {
|
getFileDownloadLink(fileId: string, cfToken: string): string {
|
||||||
return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`;
|
return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`;
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Loading from "../components/loading.tsx";
|
import Loading from "../components/loading.tsx";
|
||||||
import { CommentContent } from "../components/comment_tile.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";
|
import Badge from "../components/badge.tsx";
|
||||||
|
|
||||||
export default function ActivitiesPage() {
|
export default function ActivitiesPage() {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { useParams, useLocation, useNavigate } from "react-router";
|
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 { network } from "../network/network";
|
||||||
import showToast from "../components/toast";
|
import showToast from "../components/toast";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import ResourcesView from "../components/resources_view";
|
import ResourcesView from "../components/resources_view";
|
||||||
import Loading from "../components/loading";
|
import Loading from "../components/loading";
|
||||||
import Pagination from "../components/pagination";
|
import Pagination from "../components/pagination";
|
||||||
@@ -26,23 +26,27 @@ export default function UserPage() {
|
|||||||
const username = rawUsername ? decodeURIComponent(rawUsername) : "";
|
const username = rawUsername ? decodeURIComponent(rawUsername) : "";
|
||||||
|
|
||||||
// 从 hash 中获取当前页面,默认为 resources
|
// 从 hash 中获取当前页面,默认为 resources
|
||||||
const getPageFromHash = () => {
|
const getPageFromHash = useCallback(() => {
|
||||||
const hash = location.hash.slice(1); // 移除 # 号
|
const hash = location.hash.slice(1); // 移除 # 号
|
||||||
if (hash === "comments") return 1;
|
if (hash === "comments") return 1;
|
||||||
|
if (hash === "files") return 2;
|
||||||
return 0; // 默认为 resources
|
return 0; // 默认为 resources
|
||||||
};
|
}, [location.hash]);
|
||||||
|
|
||||||
const [page, setPage] = useState(getPageFromHash());
|
const [page, setPage] = useState(getPageFromHash());
|
||||||
|
|
||||||
// 监听 hash 变化
|
// 监听 hash 变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(getPageFromHash());
|
setPage(getPageFromHash());
|
||||||
}, [location.hash]);
|
}, [location.hash, getPageFromHash]);
|
||||||
|
|
||||||
// 更新 hash 的函数
|
// 更新 hash 的函数
|
||||||
const updateHash = (newPage: number) => {
|
const updateHash = (newPage: number) => {
|
||||||
const hash = newPage === 1 ? "#comments" : "#resources";
|
const hashs = ["resources", "comments", "files"];
|
||||||
navigate(location.pathname + hash, { replace: true });
|
const newHash = hashs[newPage] || "resources";
|
||||||
|
if (location.hash.slice(1) !== newHash) {
|
||||||
|
navigate(`/user/${username}#${newHash}`, { replace: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,10 +95,18 @@ export default function UserPage() {
|
|||||||
>
|
>
|
||||||
Comments
|
Comments
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
role="tab"
|
||||||
|
className={`tab ${page === 2 ? "tab-active" : ""}`}
|
||||||
|
onClick={() => updateHash(2)}
|
||||||
|
>
|
||||||
|
Files
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{page === 0 && <UserResources user={user} />}
|
{page === 0 && <UserResources user={user} />}
|
||||||
{page === 1 && <UserComments user={user} />}
|
{page === 1 && <UserComments user={user} />}
|
||||||
|
{page === 2 && <UserFiles user={user} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-16"></div>
|
<div className="h-16"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,3 +240,119 @@ function CommentsList({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UserFiles({ user }: { user: User }) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-2">
|
||||||
|
<FilesList
|
||||||
|
username={user.username}
|
||||||
|
page={page}
|
||||||
|
maxPageCallback={setMaxPage}
|
||||||
|
/>
|
||||||
|
{maxPage ? (
|
||||||
|
<div className={"w-full flex justify-center"}>
|
||||||
|
<Pagination page={page} setPage={setPage} totalPages={maxPage} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<RFile[] | null>(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 (
|
||||||
|
<div className={"w-full"}>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{files.map((file) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/resources/${file.resource!.id}#files`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/resources/${file.resource!.id}#files`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"card shadow p-4 my-2 hover:shadow-md transition-shadow"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h4 className={"font-bold pb-2"}>{file!.filename}</h4>
|
||||||
|
<p className={"text-sm whitespace-pre-wrap"}>
|
||||||
|
{file!.description}
|
||||||
|
</p>
|
||||||
|
<p className={"pt-1"}>
|
||||||
|
<Badge className={"badge-soft badge-secondary text-xs mr-2"}>
|
||||||
|
<MdOutlineArchive size={16} className={"inline-block"} />
|
||||||
|
{file!.is_redirect
|
||||||
|
? t("Redirect")
|
||||||
|
: fileSizeToString(file!.size)}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={"badge-soft badge-accent text-xs mr-2"}>
|
||||||
|
<MdOutlinePhotoAlbum size={16} className={"inline-block"} />
|
||||||
|
{(() => {
|
||||||
|
let title = file.resource!.title;
|
||||||
|
if (title.length > 20) {
|
||||||
|
title = title.slice(0, 20) + "...";
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
})()}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -30,6 +30,7 @@ func AddFileRoutes(router fiber.Router) {
|
|||||||
fileGroup.Delete("/:id", deleteFile)
|
fileGroup.Delete("/:id", deleteFile)
|
||||||
fileGroup.Get("/download/local", downloadLocalFile)
|
fileGroup.Get("/download/local", downloadLocalFile)
|
||||||
fileGroup.Get("/download/:id", downloadFile, middleware.NewDynamicRequestLimiter(config.MaxDownloadsPerDayForSingleIP, 24*time.Hour))
|
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,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -215,3 +215,20 @@ func SetFileStorageKeyAndSize(id string, storageKey string, size int64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@@ -21,12 +21,13 @@ type File struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileView struct {
|
type FileView struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
IsRedirect bool `json:"is_redirect"`
|
IsRedirect bool `json:"is_redirect"`
|
||||||
User UserView `json:"user"`
|
User UserView `json:"user"`
|
||||||
|
Resource *ResourceView `json:"resource,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) ToView() *FileView {
|
func (f *File) ToView() *FileView {
|
||||||
@@ -39,3 +40,21 @@ func (f *File) ToView() *FileView {
|
|||||||
User: f.User.ToView(),
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -614,3 +614,26 @@ func CreateServerDownloadTask(uid uint, url, filename, description string, resou
|
|||||||
|
|
||||||
return file.ToView(), nil
|
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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user