From 20614b20b76fd1dffb0023a5ee8025e00d59fb2f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 20 Nov 2025 19:46:33 +0800 Subject: [PATCH] feat: add component filter to marketplace page --- .../plugin-market/PluginMarketComponent.tsx | 108 ++++++++++++++---- web/src/app/infra/http/CloudServiceClient.ts | 2 + web/src/components/ui/toggle-group.tsx | 20 +++- web/src/components/ui/toggle.tsx | 2 +- web/src/i18n/locales/en-US.ts | 2 + web/src/i18n/locales/ja-JP.ts | 2 + web/src/i18n/locales/zh-Hans.ts | 2 + web/src/i18n/locales/zh-Hant.ts | 2 + 8 files changed, 111 insertions(+), 29 deletions(-) diff --git a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx index 8012cb6d..0b66be6e 100644 --- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx @@ -10,7 +10,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Search, Loader2 } from 'lucide-react'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { Search, Loader2, Wrench, AudioWaveform, Hash } from 'lucide-react'; import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent'; import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO'; import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog'; @@ -38,6 +39,7 @@ function MarketPageContent({ const searchParams = useSearchParams(); const [searchQuery, setSearchQuery] = useState(''); + const [componentFilter, setComponentFilter] = useState('all'); const [plugins, setPlugins] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); @@ -127,23 +129,18 @@ function MarketPageContent({ try { let response; const { sortBy, sortOrder } = getCurrentSort(); + const filterValue = + componentFilter === 'all' ? undefined : componentFilter; - if (isSearch && searchQuery.trim()) { - response = await getCloudServiceClientSync().searchMarketplacePlugins( - searchQuery.trim(), - page, - pageSize, - sortBy, - sortOrder, - ); - } else { - response = await getCloudServiceClientSync().getMarketplacePlugins( - page, - pageSize, - sortBy, - sortOrder, - ); - } + // Always use searchMarketplacePlugins to support component filtering + response = await getCloudServiceClientSync().searchMarketplacePlugins( + isSearch && searchQuery.trim() ? searchQuery.trim() : '', + page, + pageSize, + sortBy, + sortOrder, + filterValue, + ); const data: ApiRespMarketplacePlugins = response; const newPlugins = data.plugins.map(transformToVO); @@ -168,7 +165,14 @@ function MarketPageContent({ setIsLoadingMore(false); } }, - [searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort], + [ + searchQuery, + componentFilter, + pageSize, + transformToVO, + plugins.length, + getCurrentSort, + ], ); // 初始加载 @@ -213,10 +217,18 @@ function MarketPageContent({ // fetchPlugins will be called by useEffect when sortOption changes }, []); - // 当排序选项变化时重新加载数据 + // 组件筛选变化处理 + const handleComponentFilterChange = useCallback((value: string) => { + setComponentFilter(value); + setCurrentPage(1); + setPlugins([]); + // fetchPlugins will be called by useEffect when componentFilter changes + }, []); + + // 当排序选项或组件筛选变化时重新加载数据 useEffect(() => { fetchPlugins(1, !!searchQuery.trim(), true); - }, [sortOption]); + }, [sortOption, componentFilter]); // 处理URL参数,检查是否需要打开插件详情对话框 useEffect(() => { @@ -343,9 +355,59 @@ function MarketPageContent({ - {/* Sort dropdown */} -
-
+ {/* Component filter and sort */} +
+ {/* Component filter */} +
+ + {t('market.filterByComponent')}: + + { + if (value) handleComponentFilterChange(value); + }} + className="justify-start" + > + + {t('market.allComponents')} + + + + {t('plugins.componentName.Tool')} + + + + {t('plugins.componentName.Command')} + + + + {t('plugins.componentName.EventListener')} + + +
+ + {/* Sort dropdown */} +
{t('market.sortBy')}: diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index 401f664a..a1eb38e3 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient { page_size: number, sort_by?: string, sort_order?: string, + component_filter?: string, ): Promise { return this.post( '/api/v1/marketplace/plugins/search', @@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient { page_size, sort_by, sort_order, + component_filter, }, ); } diff --git a/web/src/components/ui/toggle-group.tsx b/web/src/components/ui/toggle-group.tsx index a15b9bcb..cecc8130 100644 --- a/web/src/components/ui/toggle-group.tsx +++ b/web/src/components/ui/toggle-group.tsx @@ -8,32 +8,40 @@ import { cn } from '@/lib/utils'; import { toggleVariants } from '@/components/ui/toggle'; const ToggleGroupContext = React.createContext< - VariantProps + VariantProps & { + spacing?: number; + } >({ size: 'default', variant: 'default', + spacing: 0, }); function ToggleGroup({ className, variant, size, + spacing = 0, children, ...props }: React.ComponentProps & - VariantProps) { + VariantProps & { + spacing?: number; + }) { return ( - + {children} @@ -55,12 +63,14 @@ function ToggleGroupItem({ data-slot="toggle-group-item" data-variant={context.variant || variant} data-size={context.size || size} + data-spacing={context.spacing} className={cn( toggleVariants({ variant: context.variant || variant, size: context.size || size, }), - 'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l', + 'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10', + 'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l', className, )} {...props} diff --git a/web/src/components/ui/toggle.tsx b/web/src/components/ui/toggle.tsx index 0fdbf91f..828f8d8f 100644 --- a/web/src/components/ui/toggle.tsx +++ b/web/src/components/ui/toggle.tsx @@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const toggleVariants = cva( - "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:data-[state=on]:bg-slate-700 dark:data-[state=on]:text-white [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", { variants: { variant: { diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index d674c22b..491393ed 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -351,6 +351,8 @@ const enUS = { markAsRead: 'Mark as Read', markAsReadSuccess: 'Marked as read', markAsReadFailed: 'Mark as read failed', + filterByComponent: 'Component', + allComponents: 'All Components', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 59ba2c3c..80f4a103 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -353,6 +353,8 @@ const jaJP = { markAsRead: '既読', markAsReadSuccess: '既読に設定しました', markAsReadFailed: '既読に設定に失敗しました', + filterByComponent: 'コンポーネント', + allComponents: '全部コンポーネント', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 6ea06616..0174da88 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -335,6 +335,8 @@ const zhHans = { markAsRead: '已读', markAsReadSuccess: '已标记为已读', markAsReadFailed: '标记为已读失败', + filterByComponent: '组件', + allComponents: '全部组件', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 14ed50bd..d5459761 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -333,6 +333,8 @@ const zhHant = { markAsRead: '已讀', markAsReadSuccess: '已標記為已讀', markAsReadFailed: '標記為已讀失敗', + filterByComponent: '組件', + allComponents: '全部組件', }, mcp: { title: 'MCP',