import { useParams, useLocation, useNavigate } from "react-router"; import { Collection, CommentWithResource, PageResponse, RFile, User, } from "../network/models"; import { network } from "../network/network"; import showToast from "../components/toast"; import { useCallback, useEffect, useState } from "react"; import ResourcesView from "../components/resources_view"; import Loading from "../components/loading"; import Pagination from "../components/pagination"; import { CommentTile } from "../components/comment_tile.tsx"; import Badge from "../components/badge.tsx"; import { MdOutlineAdd, MdOutlineArchive, MdOutlineComment, MdOutlinePhotoAlbum, } from "react-icons/md"; import { useTranslation } from "../utils/i18n"; import { app } from "../app.ts"; import Markdown from "react-markdown"; import { Debounce } from "../utils/debounce.ts"; export default function UserPage() { const [user, setUser] = useState(null); const { username: rawUsername } = useParams(); const location = useLocation(); const navigate = useNavigate(); const { t } = useTranslation(); // 解码用户名,确保特殊字符被还原 const username = rawUsername ? decodeURIComponent(rawUsername) : ""; // 从 hash 中获取当前页面,默认为 collections const getPageFromHash = useCallback(() => { const hash = location.hash.slice(1); // 移除 # 号 const hashs = ["collections", "resources", "comments", "files"]; const index = hashs.indexOf(hash); return index !== -1 ? index : 0; // 如果 hash 不在预定义的列表中,默认为 0 }, [location.hash]); const [page, setPage] = useState(getPageFromHash()); // 监听 hash 变化 useEffect(() => { setPage(getPageFromHash()); }, [location.hash, getPageFromHash]); // 更新 hash 的函数 const updateHash = (newPage: number) => { const hashs = ["collections", "resources", "comments", "files"]; const newHash = hashs[newPage] || "collections"; if (location.hash.slice(1) !== newHash) { navigate(`/user/${username}#${newHash}`, { replace: true }); } }; useEffect(() => { const preFetchData = app.getPreFetchData(); if (preFetchData?.user?.username === username) { setUser(preFetchData.user); return; } network.getUserInfo(username || "").then((res) => { if (res.success) { setUser(res.data!); } else { showToast({ message: res.message, type: "error", }); } }); }, [username]); useEffect(() => { document.title = username || "User"; }, [username]); if (!user) { return (
); } return (
updateHash(0)} > {t("Collections")}
updateHash(1)} > {t("Resources")}
updateHash(2)} > {t("Comments")}
updateHash(3)} > {t("Files")}
{page === 0 && } {page === 1 && } {page === 2 && } {page === 3 && }
); } function UserCard({ user }: { user: User }) { const { t } = useTranslation(); const statistics = (

{t("Resources")} {user.resources_count} {t("Files")} {user.files_count} {t("Comments")} {user.comments_count}

); const haveBio = user.bio.trim() !== ""; return ( <>
{"avatar"}

{user.username}

{haveBio ? (

{user.bio.trim()}

) : ( statistics )}
{haveBio &&
{statistics}
} ); } function UserResources({ user }: { user: User }) { return ( { return network.getResourcesByUser(user.username, page); }} > ); } function UserComments({ user }: { user: User }) { const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(0); return (
{maxPage ? (
) : null}
); } function CommentsList({ username, page, maxPageCallback, }: { username: string; page: number; maxPageCallback: (maxPage: number) => void; }) { const [comments, setComments] = useState(null); useEffect(() => { network.listCommentsByUser(username, page).then((res) => { if (res.success) { setComments(res.data!); maxPageCallback(res.totalPages || 1); } else { showToast({ message: res.message, type: "error", }); } }); }, [maxPageCallback, page, username]); if (comments == null) { return (
); } return ( <> {comments.map((comment) => { return ( ); })} ); } 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.replaceAll("\n", " \n")}

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

); })} ); } function Collections({ username }: { username?: string }) { const [searchKeyword, setSearchKeyword] = useState(""); const [realSearchKeyword, setRealSearchKeyword] = useState(""); const { t } = useTranslation(); const navigate = useNavigate(); const debounce = new Debounce(500); const delayedSetSearchKeyword = (keyword: string) => { setSearchKeyword(keyword); debounce.run(() => { setRealSearchKeyword(keyword); }); }; return ( <>
delayedSetSearchKeyword(e.target.value)} /> {username == app.user?.username && }
); } async function getOrSearchUserCollections( username: string, keyword: string, page: number, ): Promise> { if (keyword.trim() === "") { return network.listUserCollections(username, page); } else { let res = await network.searchUserCollections(username, keyword); return { success: res.success, data: res.data || [], totalPages: 1, message: res.message || "", }; } } function CollectionsList({ username, keyword, }: { username?: string; keyword: string; }) { const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(1); const [collections, setCollections] = useState(null); useEffect(() => { if (!username) return; setCollections(null); getOrSearchUserCollections(username, keyword, page).then((res) => { if (res.success) { setCollections(res.data! || []); setMaxPage(res.totalPages || 1); } else { showToast({ message: res.message, type: "error", }); } }); }, [username, keyword, page]); if (collections == null) { return (
); } return ( <> {collections.map((collection) => { return ; })} {maxPage > 1 ? (
) : null} ); } function CollectionCard({ collection }: { collection: Collection }) { const navigate = useNavigate(); const { t } = useTranslation(); return (
{ navigate(`/collection/${collection.id}`); }} >

{collection.title}

{collection.resources_count} {t("Resources")}
); } function CollectionContent({ content }: { content: string }) { const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (!line.endsWith(" ")) { // Ensure that each line ends with two spaces for Markdown to recognize it as a line break lines[i] = line + " "; } } content = lines.join("\n"); return {content}; }