mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: implement sort in plugin market
This commit is contained in:
@@ -4,8 +4,18 @@ import { useEffect, useState } from 'react';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO';
|
||||
import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent';
|
||||
import { Input, Pagination } from 'antd';
|
||||
import { spaceClient } from '@/app/infra/http/HttpClient';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
export default function PluginMarketComponent({
|
||||
askInstallPlugin,
|
||||
@@ -19,6 +29,8 @@ export default function PluginMarketComponent({
|
||||
const [nowPage, setNowPage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sortByValue, setSortByValue] = useState<string>('stars');
|
||||
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
|
||||
const pageSize = 10;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,10 +52,12 @@ export default function PluginMarketComponent({
|
||||
function getPluginList(
|
||||
page: number = nowPage,
|
||||
keyword: string = searchKeyword,
|
||||
sortBy: string = sortByValue,
|
||||
sortOrder: string = sortOrderValue,
|
||||
) {
|
||||
setLoading(true);
|
||||
spaceClient
|
||||
.getMarketPlugins(page, pageSize, keyword)
|
||||
.getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder)
|
||||
.then((res) => {
|
||||
setMarketPluginList(
|
||||
res.plugins.map(
|
||||
@@ -89,23 +103,97 @@ export default function PluginMarketComponent({
|
||||
getPluginList(page);
|
||||
}
|
||||
|
||||
function handleSortChange(value: string) {
|
||||
const [newSortBy, newSortOrder] = value.split(',').map(s => s.trim());
|
||||
setSortByValue(newSortBy);
|
||||
setSortOrderValue(newSortOrder);
|
||||
setNowPage(1);
|
||||
getPluginList(1, searchKeyword, newSortBy, newSortOrder);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.marketComponentBody}`}>
|
||||
<Input
|
||||
style={{
|
||||
width: '300px',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center justify-start mb-2 mt-2">
|
||||
<Input
|
||||
style={{
|
||||
width: '300px',
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
<Select value={`${sortByValue},${sortOrderValue}`} onValueChange={handleSortChange}>
|
||||
<SelectTrigger className="w-[180px] ml-2">
|
||||
<SelectValue placeholder="排序方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stars,DESC">最多星标</SelectItem>
|
||||
<SelectItem value="created_at,DESC">最近新增</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">最近更新</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center justify-end ml-2">
|
||||
{totalCount > 0 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => handlePageChange(nowPage - 1)}
|
||||
className={nowPage <= 1 ? 'pointer-events-none opacity-50' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */}
|
||||
{(() => {
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
const maxVisiblePages = 5;
|
||||
let startPage = Math.max(1, nowPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
return Array.from({ length: endPage - startPage + 1 }, (_, i) => {
|
||||
const pageNum = startPage + i;
|
||||
return (
|
||||
<PaginationItem key={pageNum}>
|
||||
<PaginationLink
|
||||
isActive={pageNum === nowPage}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
<span className="text-black select-none">
|
||||
{pageNum}
|
||||
</span>
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(nowPage + 1)}
|
||||
className={nowPage >= Math.ceil(totalCount / pageSize) ? 'pointer-events-none opacity-50' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>加载中...</div>
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{/* 加载中... */}
|
||||
</div>
|
||||
) : marketPluginList.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
没有找到匹配的插件
|
||||
{/* 没有找到匹配的插件 */}
|
||||
</div>
|
||||
) : (
|
||||
marketPluginList.map((vo, index) => (
|
||||
@@ -117,24 +205,6 @@ export default function PluginMarketComponent({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{totalCount > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
marginTop: '20px',
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
current={nowPage}
|
||||
total={totalCount}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -360,13 +360,15 @@ class HttpClient {
|
||||
page: number,
|
||||
page_size: number,
|
||||
query: string,
|
||||
sort_by: string = 'stars',
|
||||
sort_order: string = 'DESC',
|
||||
): Promise<MarketPluginResponse> {
|
||||
return this.post(`/api/v1/market/plugins`, {
|
||||
page,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: 'stars',
|
||||
sort_order: 'DESC',
|
||||
sort_by,
|
||||
sort_order,
|
||||
});
|
||||
}
|
||||
public installPluginFromGithub(
|
||||
|
||||
127
web/src/components/ui/pagination.tsx
Normal file
127
web/src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="text-black" />
|
||||
<span className="hidden sm:block"></span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block"></span>
|
||||
<ChevronRightIcon className="text-black" />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
Reference in New Issue
Block a user