fix(market): sync plugin market UI improvements from Space (#2056)

* fix(market): sync plugin market UI from space - page size 12, full list display, fix double separator, adaptive tag display

* fix: lint and prettier formatting

* fix: prettier formatting for remaining files
This commit is contained in:
Junyan Chin
2026-03-12 15:06:11 +08:00
committed by GitHub
parent 79311ccde3
commit 8b8cfb76de
3 changed files with 65 additions and 56 deletions
@@ -1,13 +1,6 @@
'use client'; 'use client';
import { import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
useState,
useEffect,
useCallback,
useRef,
Suspense,
useMemo,
} from 'react';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
Select, Select,
@@ -70,7 +63,7 @@ function MarketPageContent({
RecommendationList[] RecommendationList[]
>([]); >([]);
const pageSize = 16; // 每页16个,4行x4列 const pageSize = 12; // 每页12个
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null); const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null); const scrollContainerRef = useRef<HTMLDivElement | null>(null);
@@ -330,38 +323,7 @@ function MarketPageContent({
}; };
}, []); }, []);
// 计算所有推荐插件的 ID 集合 const visiblePlugins = plugins;
const recommendedPluginIds = useMemo(() => {
const ids = new Set<string>();
recommendationLists.forEach((list) => {
list.plugins.forEach((plugin) => {
ids.add(`${plugin.author} / ${plugin.name}`);
});
});
return ids;
}, [recommendationLists]);
// 过滤掉已在推荐列表中展示的插件
// 仅在显示推荐列表的条件下(无搜索、无筛选、第一页或后续页的累积数据中)进行过滤
// 注意:如果用户翻页,我们希望一直保持去重,否则推荐过的插件会在第二页出现
// 但是推荐列表只在第一页且无筛选时显示。
// 如果用户进行了筛选/搜索,推荐列表不显示,此时不需要去重。
const visiblePlugins = useMemo(() => {
const showRecommendations =
!searchQuery && componentFilter === 'all' && selectedTags.length === 0;
if (!showRecommendations) {
return plugins;
}
return plugins.filter((p) => !recommendedPluginIds.has(p.pluginId));
}, [
plugins,
recommendedPluginIds,
searchQuery,
componentFilter,
selectedTags,
]);
// 加载更多 // 加载更多
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
@@ -47,10 +47,12 @@ function RecommendationListRow({
list, list,
tagNames, tagNames,
onInstall, onInstall,
isLast,
}: { }: {
list: RecommendationList; list: RecommendationList;
tagNames: Record<string, string>; tagNames: Record<string, string>;
onInstall: (author: string, pluginName: string) => void; onInstall: (author: string, pluginName: string) => void;
isLast: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@@ -143,7 +145,9 @@ function RecommendationListRow({
/> />
))} ))}
</div> </div>
{totalPages > 1 && <div className="border-b border-border mt-6" />} {totalPages > 1 && !isLast && (
<div className="border-b border-border mt-6" />
)}
</div> </div>
); );
} }
@@ -161,12 +165,13 @@ export function RecommendationLists({
return ( return (
<div className="mt-6"> <div className="mt-6">
{lists.map((list) => ( {lists.map((list, index) => (
<RecommendationListRow <RecommendationListRow
key={list.uuid} key={list.uuid}
list={list} list={list}
tagNames={tagNames} tagNames={tagNames}
onInstall={onInstall} onInstall={onInstall}
isLast={index === lists.length - 1}
/> />
))} ))}
<div className="border-b border-border mb-6" /> <div className="border-b border-border mb-6" />
@@ -17,7 +17,7 @@ import {
FileText, FileText,
Info, Info,
} from 'lucide-react'; } from 'lucide-react';
import { useState } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
export default function PluginMarketCardComponent({ export default function PluginMarketCardComponent({
@@ -31,6 +31,43 @@ export default function PluginMarketCardComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null);
const [visibleTags, setVisibleTags] = useState(2);
// Measure how many tags fit in the bottom row
useEffect(() => {
const tags = cardVO.tags;
if (!bottomRef.current || !tags || tags.length === 0) return;
const measure = () => {
const container = bottomRef.current;
if (!container) return;
const width = container.offsetWidth;
const availableForTags = width - 140 - 80;
if (availableForTags <= 0) {
setVisibleTags(0);
return;
}
const tagWidth = 80;
const plusBadgeWidth = 40;
const maxTags = Math.max(
0,
Math.floor((availableForTags - plusBadgeWidth) / tagWidth),
);
if (maxTags >= tags.length) {
setVisibleTags(tags.length);
} else {
setVisibleTags(Math.max(1, maxTags));
}
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(bottomRef.current);
return () => observer.disconnect();
}, [cardVO.tags]);
const remainingTags = cardVO.tags ? cardVO.tags.length - visibleTags : 0;
function handleInstallClick(e: React.MouseEvent) { function handleInstallClick(e: React.MouseEvent) {
e.stopPropagation(); e.stopPropagation();
@@ -135,10 +172,13 @@ export default function PluginMarketCardComponent({
</div> </div>
{/* 下部分:下载量、标签和组件列表 */} {/* 下部分:下载量、标签和组件列表 */}
<div className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0"> <div
<div className="flex flex-row items-center justify-start gap-2 flex-wrap"> ref={bottomRef}
className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0 overflow-hidden"
>
<div className="flex flex-row items-center justify-start gap-2 min-w-0 overflow-hidden">
{/* 下载数量 */} {/* 下载数量 */}
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem]"> <div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem] flex-shrink-0">
<svg <svg
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0" className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -156,14 +196,14 @@ export default function PluginMarketCardComponent({
</div> </div>
</div> </div>
{/* Tags */} {/* Tags - adaptive */}
{cardVO.tags && cardVO.tags.length > 0 && ( {cardVO.tags && cardVO.tags.length > 0 && visibleTags > 0 && (
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-row items-center gap-1.5 overflow-hidden flex-shrink min-w-0">
{cardVO.tags.slice(0, 2).map((tag) => ( {cardVO.tags.slice(0, visibleTags).map((tag) => (
<Badge <Badge
key={tag} key={tag}
variant="secondary" variant="secondary"
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center gap-1 flex-shrink-0" className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center gap-1 flex-shrink-0 whitespace-nowrap"
> >
<svg <svg
className="w-2.5 h-2.5 flex-shrink-0" className="w-2.5 h-2.5 flex-shrink-0"
@@ -178,15 +218,17 @@ export default function PluginMarketCardComponent({
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /> <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
<line x1="7" y1="7" x2="7.01" y2="7" /> <line x1="7" y1="7" x2="7.01" y2="7" />
</svg> </svg>
<span className="truncate">{tagNames[tag] || tag}</span> <span className="truncate max-w-[5rem]">
{tagNames[tag] || tag}
</span>
</Badge> </Badge>
))} ))}
{cardVO.tags.length > 2 && ( {remainingTags > 0 && (
<Badge <Badge
variant="outline" variant="outline"
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center flex-shrink-0" className="text-[0.65rem] sm:text-[0.7rem] px-1.5 py-0.5 h-5 flex items-center flex-shrink-0 whitespace-nowrap"
> >
+{cardVO.tags.length - 2} +{remainingTags}
</Badge> </Badge>
)} )}
</div> </div>