Add AboutPage component and integrate sorting options in resource retrieval

This commit is contained in:
2025-05-25 11:27:24 +08:00
parent b610a5db9d
commit 6e78707d9e
11 changed files with 139 additions and 27 deletions

View File

@@ -10,6 +10,7 @@ import ManagePage from "./pages/manage_page.tsx";
import TaggedResourcesPage from "./pages/tagged_resources_page.tsx"; import TaggedResourcesPage from "./pages/tagged_resources_page.tsx";
import UserPage from "./pages/user_page.tsx"; import UserPage from "./pages/user_page.tsx";
import EditResourcePage from "./pages/edit_resource_page.tsx"; import EditResourcePage from "./pages/edit_resource_page.tsx";
import AboutPage from "./pages/about_page.tsx";
export default function App() { export default function App() {
return ( return (
@@ -26,6 +27,7 @@ export default function App() {
<Route path={"/tag/:tag"} element={<TaggedResourcesPage/>}/> <Route path={"/tag/:tag"} element={<TaggedResourcesPage/>}/>
<Route path={"/user/:username"} element={<UserPage/>}/> <Route path={"/user/:username"} element={<UserPage/>}/>
<Route path={"/resource/edit/:rid"} element={<EditResourcePage/>}/> <Route path={"/resource/edit/:rid"} element={<EditResourcePage/>}/>
<Route path={"/about"} element={<AboutPage/>}/>
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@@ -319,6 +319,15 @@ export const i18nData = {
"Set the description of the tag.": "设置标签的描述。", "Set the description of the tag.": "设置标签的描述。",
"Use markdown format.": "使用Markdown格式。", "Use markdown format.": "使用Markdown格式。",
"Tag: ": "标签: ", "Tag: ": "标签: ",
// 添加排序选项翻译
"Select a Order": "选择排序方式",
"Latest": "最新",
"Oldest": "最早",
"Most Viewed": "浏览最多",
"Least Viewed": "浏览最少",
"Most Downloaded": "下载最多",
"Least Downloaded": "下载最少",
} }
}, },
"zh-TW": { "zh-TW": {
@@ -480,6 +489,15 @@ export const i18nData = {
"Set the description of the tag.": "設置標籤的描述。", "Set the description of the tag.": "設置標籤的描述。",
"Use markdown format.": "使用Markdown格式。", "Use markdown format.": "使用Markdown格式。",
"Tag: ": "標籤: ", "Tag: ": "標籤: ",
// 添加排序选项翻译
"Select a Order": "選擇排序方式",
"Latest": "最新",
"Oldest": "最早",
"Most Viewed": "瀏覽最多",
"Least Viewed": "瀏覽最少",
"Most Downloaded": "下載最多",
"Least Downloaded": "下載最少",
} }
} }
} }

View File

@@ -84,6 +84,9 @@ article {
border-radius: 4px; border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, system-ui; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, system-ui;
} }
.line-break {
white-space: pre-wrap;
}
} }
a.no-underline { a.no-underline {

View File

@@ -126,3 +126,12 @@ export interface ServerConfig {
server_description: string; server_description: string;
site_info: string; site_info: string;
} }
export enum RSort {
TimeAsc = 0,
TimeDesc = 1,
ViewsAsc = 2,
ViewsDesc = 3,
DownloadsAsc = 4,
DownloadsDesc = 5,
}

View File

@@ -14,7 +14,7 @@ import {
UserWithToken, UserWithToken,
Comment, Comment,
CommentWithResource, CommentWithResource,
ServerConfig ServerConfig, RSort
} from "./models.ts"; } from "./models.ts";
class Network { class Network {
@@ -409,11 +409,12 @@ class Network {
} }
} }
async getResources(page: number): Promise<PageResponse<Resource>> { async getResources(page: number, sort: RSort): Promise<PageResponse<Resource>> {
try { try {
const response = await axios.get(`${this.apiBaseUrl}/resource`, { const response = await axios.get(`${this.apiBaseUrl}/resource`, {
params: { params: {
page page,
sort,
} }
}) })
return response.data return response.data

View File

@@ -0,0 +1,10 @@
import Markdown from "react-markdown";
import {app} from "../app.ts";
export default function AboutPage() {
return <article className={"p-4"}>
<Markdown>
{app.siteInfo}
</Markdown>
</article>
}

View File

@@ -2,36 +2,63 @@ import {useEffect, useState} from "react";
import ResourcesView from "../components/resources_view.tsx"; import ResourcesView from "../components/resources_view.tsx";
import {network} from "../network/network.ts"; import {network} from "../network/network.ts";
import { app } from "../app.ts"; import { app } from "../app.ts";
import Markdown from "react-markdown"; import {RSort} from "../network/models.ts";
import Button from "../components/button.tsx";
import {MdInfoOutline} from "react-icons/md";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useNavigate} from "react-router";
export default function HomePage() { export default function HomePage() {
useEffect(() => { useEffect(() => {
document.title = app.appName; document.title = app.appName;
}, []) }, [])
const [isCollapsed, setIsCollapsed] = useState(false); const [order, setOrder] = useState(RSort.TimeAsc)
const {t} = useTranslation() const {t} = useTranslation()
const navigate = useNavigate()
return <> return <>
{ <div className={"flex p-4 items-center"}>
app.siteInfo && <div className={"mt-4 px-4"}> <select value={order} className="select w-52 select-info" onInput={(e) => {
<div className="collapse collapse-arrow bg-base-100 border border-base-300" onClick={() => setIsCollapsed(!isCollapsed)}> const value = e.currentTarget.value;
<input type="radio" name="my-accordion-2" checked={isCollapsed} style={{ if (value === "0") {
"cursor": "pointer", setOrder(RSort.TimeAsc);
}}/> } else if (value === "1") {
<div className="collapse-title font-semibold cursor-pointer">{t("About this site")}</div> setOrder(RSort.TimeDesc);
<article className="collapse-content text-sm cursor-auto" onClick={(e) => { } else if (value === "2") {
e.stopPropagation(); setOrder(RSort.ViewsDesc);
}}> } else if (value === "3") {
<Markdown> setOrder(RSort.ViewsAsc);
{app.siteInfo} } else if (value === "4") {
</Markdown> setOrder(RSort.DownloadsDesc);
</article> } else if (value === "5") {
setOrder(RSort.DownloadsAsc);
}
}}>
<option disabled>{t("Select a Order")}</option>
<option value={0}>{t("Latest")}</option>
<option value={1}>{t("Oldest")}</option>
<option value={2}>{t("Most Viewed")}</option>
<option value={3}>{t("Least Viewed")}</option>
<option value={4}>{t("Most Downloaded")}</option>
<option value={5}>{t("Least Downloaded")}</option>
</select>
<span className={"flex-1"}/>
<Button onClick={() => {
navigate("/about");
}}>
<div className={"flex items-center"}>
<MdInfoOutline size={24} className={"inline-block mr-2"}/>
<span>{t("About this site")}</span>
</div> </div>
</div> </Button>
} </div>
<ResourcesView storageKey={"home_page"} loader={(page) => network.getResources(page)}></ResourcesView> <ResourcesView
key={`home_page_${order}`}
storageKey={`home_page_${order}`}
loader={(page) => network.getResources(page, order)}
/>
</> </>
} }

View File

@@ -101,7 +101,19 @@ func handleListResources(c fiber.Ctx) error {
if err != nil { if err != nil {
return model.NewRequestError("Invalid page number") return model.NewRequestError("Invalid page number")
} }
resources, maxPage, err := service.GetResourceList(page) sortStr := c.Query("sort")
if sortStr == "" {
sortStr = "0"
}
sortInt, err := strconv.Atoi(sortStr)
if err != nil {
return model.NewRequestError("Invalid sort parameter")
}
if sortInt < 0 || sortInt > 5 {
return model.NewRequestError("Sort parameter out of range")
}
sort := model.RSort(sortInt)
resources, maxPage, err := service.GetResourceList(page, sort)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -37,7 +37,7 @@ func GetResourceByID(id uint) (model.Resource, error) {
return r, nil return r, nil
} }
func GetResourceList(page, pageSize int) ([]model.Resource, int, error) { func GetResourceList(page, pageSize int, sort model.RSort) ([]model.Resource, int, error) {
// Retrieve a list of resources with pagination // Retrieve a list of resources with pagination
var resources []model.Resource var resources []model.Resource
var total int64 var total int64
@@ -46,7 +46,25 @@ func GetResourceList(page, pageSize int) ([]model.Resource, int, error) {
return nil, 0, err return nil, 0, err
} }
if err := db.Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Preload("Tags").Order("created_at DESC").Find(&resources).Error; err != nil { order := ""
switch sort {
case model.RSortTimeAsc:
order = "created_at ASC"
case model.RSortTimeDesc:
order = "created_at DESC"
case model.RSortViewsAsc:
order = "views ASC"
case model.RSortViewsDesc:
order = "views DESC"
case model.RSortDownloadsAsc:
order = "downloads ASC"
case model.RSortDownloadsDesc:
order = "downloads DESC"
default:
order = "created_at DESC" // Default sort order
}
if err := db.Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Preload("Tags").Order(order).Find(&resources).Error; err != nil {
return nil, 0, err return nil, 0, err
} }

12
server/model/sort.go Normal file
View File

@@ -0,0 +1,12 @@
package model
type RSort uint8
const (
RSortTimeAsc RSort = iota
RSortTimeDesc
RSortViewsAsc
RSortViewsDesc
RSortDownloadsAsc
RSortDownloadsDesc
)

View File

@@ -125,8 +125,8 @@ func GetResource(id uint, host string) (*model.ResourceDetailView, error) {
return &v, nil return &v, nil
} }
func GetResourceList(page int) ([]model.ResourceView, int, error) { func GetResourceList(page int, sort model.RSort) ([]model.ResourceView, int, error) {
resources, totalPages, err := dao.GetResourceList(page, pageSize) resources, totalPages, err := dao.GetResourceList(page, pageSize, sort)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }