feat(plugin-market): rename component filter to "插件组件" with hint tooltip + persist filters

- Rename the in-app plugin market component filter label to "插件组件" /
  "Plugin Component"
- Add an Info icon tooltip explaining what plugin components are (Tool /
  Command / EventListener, etc.)
- Persist filter selections (type / component / tags / sort) in localStorage
  so they survive reloads; restored on mount (URL type param still wins)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Junyan Qin
2026-06-01 23:14:59 +08:00
parent 67fab1dac6
commit 4a68b21477
3 changed files with 72 additions and 7 deletions

View File

@@ -24,7 +24,13 @@ import {
FileText,
SlidersHorizontal,
X,
Info,
} from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
import { RecommendationLists } from './RecommendationLists';
@@ -49,6 +55,23 @@ interface SortOption {
sortOrder: string;
}
// Persist the market filter conditions (type / component / tags / sort) across
// visits via localStorage.
const MARKET_FILTERS_KEY = 'langbot_market_filters';
interface MarketFilters {
typeFilter?: string;
componentFilter?: string;
selectedTags?: string[];
sortOption?: string;
}
function loadMarketFilters(): MarketFilters {
try {
return JSON.parse(localStorage.getItem(MARKET_FILTERS_KEY) || '{}');
} catch {
return {};
}
}
// 内部组件,用于处理搜索参数
function MarketPageContent({
installPlugin,
@@ -70,17 +93,22 @@ function MarketPageContent({
];
const [searchQuery, setSearchQuery] = useState('');
const [componentFilter, setComponentFilter] = useState('all');
const [componentFilter, setComponentFilter] = useState<string>(
() => loadMarketFilters().componentFilter ?? 'all',
);
const [typeFilter, setTypeFilter] = useState<string>(() => {
const type = searchParams.get('type');
if (type && validTypes.includes(type)) {
return type;
}
return 'all';
const saved = loadMarketFilters().typeFilter;
return saved && validTypes.includes(saved) ? saved : 'all';
});
const activeAdvancedFilters =
(typeFilter === 'all' ? 0 : 1) + (componentFilter === 'all' ? 0 : 1);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedTags, setSelectedTags] = useState<string[]>(
() => loadMarketFilters().selectedTags ?? [],
);
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
const [tagNames, setTagNames] = useState<Record<string, string>>({});
const [recommendationLists, setRecommendationLists] = useState<
@@ -92,7 +120,26 @@ function MarketPageContent({
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const [sortOption, setSortOption] = useState('install_count_desc');
const [sortOption, setSortOption] = useState<string>(
() => loadMarketFilters().sortOption ?? 'install_count_desc',
);
// Persist filter conditions so they survive navigation / reload.
useEffect(() => {
try {
localStorage.setItem(
MARKET_FILTERS_KEY,
JSON.stringify({
typeFilter,
componentFilter,
selectedTags,
sortOption,
}),
);
} catch {
// ignore storage errors
}
}, [typeFilter, componentFilter, selectedTags, sortOption]);
const pageSize = 12; // 每页12个
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -642,8 +689,22 @@ function MarketPageContent({
</div>
<Separator />
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground">
<div className="flex items-center gap-1 text-xs font-medium text-muted-foreground">
{t('market.filterByComponent')}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex text-muted-foreground/70 hover:text-foreground"
aria-label={t('market.filterByComponentHint')}
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-64">
{t('market.filterByComponentHint')}
</TooltipContent>
</Tooltip>
</div>
<ToggleGroup
type="single"

View File

@@ -670,7 +670,9 @@ const enUS = {
markAsRead: 'Mark as Read',
markAsReadSuccess: 'Marked as read',
markAsReadFailed: 'Mark as read failed',
filterByComponent: 'Component',
filterByComponent: 'Plugin Component',
filterByComponentHint:
'Plugin components are the capability types a plugin provides — Tool, Command, EventListener, etc. Filter by component to show only plugins offering that capability.',
allComponents: 'All Components',
componentName: {
Tool: 'Tool',

View File

@@ -643,7 +643,9 @@ const zhHans = {
markAsRead: '已读',
markAsReadSuccess: '已标记为已读',
markAsReadFailed: '标记为已读失败',
filterByComponent: '组件',
filterByComponent: '插件组件',
filterByComponentHint:
'插件组件是插件提供的能力类型如工具Tool、命令Command、事件监听器EventListener等。按组件筛选可只看提供对应能力的插件。',
allComponents: '全部组件',
componentName: {
Tool: '工具',