feat(web): show recommendation lists in plugin market; mixed-type icons

The marketplace recommendation lists (curated rows from Space) were never
mounted in the plugin market page. Wire them in:
- fetch recommendation lists on mount and render them above the extension
  grid, only when no search/filter is active.

Recommendation lists now mix plugins, MCPs and skills, so resolve each
card's icon by type (plugin / mcp / skill marketplace icon URL) instead of
always using the plugin icon endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Junyan Qin
2026-06-01 17:48:40 +08:00
parent 33434a6712
commit 95c09688e6
2 changed files with 39 additions and 4 deletions

View File

@@ -27,6 +27,8 @@ import {
} from 'lucide-react';
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
import { RecommendationLists } from './RecommendationLists';
import type { RecommendationList } from './RecommendationLists';
import { getCloudServiceClientSync } from '@/app/infra/http';
import { useTranslation } from 'react-i18next';
import { PluginV4, PluginV4Status } from '@/app/infra/entities/plugin';
@@ -78,6 +80,9 @@ function MarketPageContent({
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
const [tagNames, setTagNames] = useState<Record<string, string>>({});
const [recommendationLists, setRecommendationLists] = useState<
RecommendationList[]
>([]);
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
@@ -250,9 +255,21 @@ function MarketPageContent({
useEffect(() => {
fetchPlugins(1, false, true);
fetchAvailableTags();
fetchRecommendationLists();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 获取推荐列表(精选,混合插件/MCP/Skill
const fetchRecommendationLists = async () => {
try {
const { lists } =
await getCloudServiceClientSync().getRecommendationLists();
setRecommendationLists(lists || []);
} catch (error) {
console.error('Failed to fetch recommendation lists:', error);
}
};
// 获取可用标签
const fetchAvailableTags = async () => {
try {
@@ -701,6 +718,18 @@ function MarketPageContent({
ref={scrollContainerRef}
className="flex-1 overflow-y-auto px-3 sm:px-4 pb-6 container mx-auto"
>
{/* 推荐列表(仅在无搜索/筛选时展示,混合插件/MCP/Skill */}
{!searchQuery &&
typeFilter === 'all' &&
componentFilter === 'all' &&
selectedTags.length === 0 && (
<RecommendationLists
lists={recommendationLists}
tagNames={tagNames}
onInstall={handleInstallPlugin}
/>
)}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<LoadingSpinner text={t('market.loading')} />

View File

@@ -22,6 +22,15 @@ function pluginToVO(
plugin: PluginV4,
t: (key: string) => string,
): PluginMarketCardVO {
const cloudClient = getCloudServiceClientSync();
// Recommendation lists are mixed-type; resolve the icon per extension type.
const iconURL =
plugin.type === 'mcp'
? cloudClient.getMCPMarketplaceIconURL(plugin.author, plugin.name)
: plugin.type === 'skill'
? cloudClient.getSkillMarketplaceIconURL(plugin.author, plugin.name)
: cloudClient.getPluginIconURL(plugin.author, plugin.name);
return new PluginMarketCardVO({
pluginId: plugin.author + ' / ' + plugin.name,
author: plugin.author,
@@ -30,10 +39,7 @@ function pluginToVO(
description:
extractI18nObject(plugin.description) || t('market.noDescription'),
installCount: plugin.install_count,
iconURL: getCloudServiceClientSync().getPluginIconURL(
plugin.author,
plugin.name,
),
iconURL,
githubURL: plugin.repository,
version: plugin.latest_version,
components: plugin.components,