mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-04 08:37:14 +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 styles from '@/app/home/plugins/plugins.module.css';
|
||||||
import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO';
|
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 PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent';
|
||||||
import { Input, Pagination } from 'antd';
|
|
||||||
import { spaceClient } from '@/app/infra/http/HttpClient';
|
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({
|
export default function PluginMarketComponent({
|
||||||
askInstallPlugin,
|
askInstallPlugin,
|
||||||
@@ -19,6 +29,8 @@ export default function PluginMarketComponent({
|
|||||||
const [nowPage, setNowPage] = useState(1);
|
const [nowPage, setNowPage] = useState(1);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [sortByValue, setSortByValue] = useState<string>('stars');
|
||||||
|
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,10 +52,12 @@ export default function PluginMarketComponent({
|
|||||||
function getPluginList(
|
function getPluginList(
|
||||||
page: number = nowPage,
|
page: number = nowPage,
|
||||||
keyword: string = searchKeyword,
|
keyword: string = searchKeyword,
|
||||||
|
sortBy: string = sortByValue,
|
||||||
|
sortOrder: string = sortOrderValue,
|
||||||
) {
|
) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
spaceClient
|
spaceClient
|
||||||
.getMarketPlugins(page, pageSize, keyword)
|
.getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setMarketPluginList(
|
setMarketPluginList(
|
||||||
res.plugins.map(
|
res.plugins.map(
|
||||||
@@ -89,23 +103,97 @@ export default function PluginMarketComponent({
|
|||||||
getPluginList(page);
|
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 (
|
return (
|
||||||
<div className={`${styles.marketComponentBody}`}>
|
<div className={`${styles.marketComponentBody}`}>
|
||||||
<Input
|
<div className="flex items-center justify-start mb-2 mt-2">
|
||||||
style={{
|
<Input
|
||||||
width: '300px',
|
style={{
|
||||||
marginBottom: '10px',
|
width: '300px',
|
||||||
}}
|
}}
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
placeholder="搜索插件"
|
placeholder="搜索插件"
|
||||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
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}`}>
|
<div className={`${styles.pluginListContainer}`}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>加载中...</div>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
|
{/* 加载中... */}
|
||||||
|
</div>
|
||||||
) : marketPluginList.length === 0 ? (
|
) : marketPluginList.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
没有找到匹配的插件
|
{/* 没有找到匹配的插件 */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
marketPluginList.map((vo, index) => (
|
marketPluginList.map((vo, index) => (
|
||||||
@@ -117,24 +205,6 @@ export default function PluginMarketComponent({
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,13 +360,15 @@ class HttpClient {
|
|||||||
page: number,
|
page: number,
|
||||||
page_size: number,
|
page_size: number,
|
||||||
query: string,
|
query: string,
|
||||||
|
sort_by: string = 'stars',
|
||||||
|
sort_order: string = 'DESC',
|
||||||
): Promise<MarketPluginResponse> {
|
): Promise<MarketPluginResponse> {
|
||||||
return this.post(`/api/v1/market/plugins`, {
|
return this.post(`/api/v1/market/plugins`, {
|
||||||
page,
|
page,
|
||||||
page_size,
|
page_size,
|
||||||
query,
|
query,
|
||||||
sort_by: 'stars',
|
sort_by,
|
||||||
sort_order: 'DESC',
|
sort_order,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public installPluginFromGithub(
|
public installPluginFromGithub(
|
||||||
|
|||||||
@@ -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