feat: add plugin components displaying in marketplace page

This commit is contained in:
Junyan Qin
2025-11-20 18:50:00 +08:00
parent 763c1a885c
commit da323817f7
9 changed files with 149 additions and 43 deletions

View File

@@ -234,7 +234,19 @@ export default function PipelineExtension({
</div> </div>
<div className="flex gap-1 mt-1"> <div className="flex gap-1 mt-1">
<PluginComponentList <PluginComponentList
components={plugin.components} components={(() => {
const componentKindCount: Record<string, number> =
{};
for (const component of plugin.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true} showComponentName={true}
showTitle={false} showTitle={false}
useBadge={true} useBadge={true}
@@ -401,7 +413,19 @@ export default function PipelineExtension({
</div> </div>
<div className="flex gap-1 mt-1"> <div className="flex gap-1 mt-1">
<PluginComponentList <PluginComponentList
components={plugin.components} components={(() => {
const componentKindCount: Record<string, number> =
{};
for (const component of plugin.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true} showComponentName={true}
showTitle={false} showTitle={false}
useBadge={true} useBadge={true}

View File

@@ -1,4 +1,3 @@
import { PluginComponent } from '@/app/infra/entities/plugin';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { Wrench, AudioWaveform, Hash } from 'lucide-react'; import { Wrench, AudioWaveform, Hash } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -9,31 +8,22 @@ export default function PluginComponentList({
showTitle, showTitle,
useBadge, useBadge,
t, t,
responsive = false,
}: { }: {
components: PluginComponent[]; components: Record<string, number>;
showComponentName: boolean; showComponentName: boolean;
showTitle: boolean; showTitle: boolean;
useBadge: boolean; useBadge: boolean;
t: TFunction; t: TFunction;
responsive?: boolean;
}) { }) {
const componentKindCount: Record<string, number> = {};
for (const component of components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
const kindIconMap: Record<string, React.ReactNode> = { const kindIconMap: Record<string, React.ReactNode> = {
Tool: <Wrench className="w-5 h-5" />, Tool: <Wrench className="w-5 h-5" />,
EventListener: <AudioWaveform className="w-5 h-5" />, EventListener: <AudioWaveform className="w-5 h-5" />,
Command: <Hash className="w-5 h-5" />, Command: <Hash className="w-5 h-5" />,
}; };
const componentKindList = Object.keys(componentKindCount); const componentKindList = Object.keys(components || {});
return ( return (
<> <>
@@ -44,11 +34,21 @@ export default function PluginComponentList({
return ( return (
<> <>
{useBadge && ( {useBadge && (
<Badge variant="outline"> <Badge
key={kind}
variant="outline"
className="flex items-center gap-1"
>
{kindIconMap[kind]} {kindIconMap[kind]}
{showComponentName && {/* 响应式显示组件名称:在中等屏幕以上显示 */}
t('plugins.componentName.' + kind) + ' '} {responsive ? (
{componentKindCount[kind]} <span className="hidden md:inline">
{t('plugins.componentName.' + kind)}
</span>
) : (
showComponentName && t('plugins.componentName.' + kind)
)}
<span className="ml-1">{components[kind]}</span>
</Badge> </Badge>
)} )}
@@ -58,9 +58,15 @@ export default function PluginComponentList({
className="flex flex-row items-center justify-start gap-[0.2rem]" className="flex flex-row items-center justify-start gap-[0.2rem]"
> >
{kindIconMap[kind]} {kindIconMap[kind]}
{showComponentName && {/* 响应式显示组件名称:在中等屏幕以上显示 */}
t('plugins.componentName.' + kind) + ' '} {responsive ? (
{componentKindCount[kind]} <span className="hidden md:inline">
{t('plugins.componentName.' + kind)}
</span>
) : (
showComponentName && t('plugins.componentName.' + kind)
)}
<span className="ml-1">{components[kind]}</span>
</div> </div>
)} )}
</> </>

View File

@@ -128,7 +128,18 @@ export default function PluginCardComponent({
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]"> <div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
<PluginComponentList <PluginComponentList
components={cardVO.components} components={(() => {
const componentKindCount: Record<string, number> = {};
for (const component of cardVO.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={false} showComponentName={false}
showTitle={true} showTitle={true}
useBadge={false} useBadge={false}

View File

@@ -160,7 +160,18 @@ export default function PluginForm({
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]"> <div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
<PluginComponentList <PluginComponentList
components={pluginInfo.components} components={(() => {
const componentKindCount: Record<string, number> = {};
for (const component of pluginInfo.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true} showComponentName={true}
showTitle={false} showTitle={false}
useBadge={true} useBadge={true}

View File

@@ -111,6 +111,7 @@ function MarketPageContent({
), ),
githubURL: plugin.repository, githubURL: plugin.repository,
version: plugin.latest_version, version: plugin.latest_version,
components: plugin.components,
}); });
}, []); }, []);

View File

@@ -12,6 +12,7 @@ import { toast } from 'sonner';
import { PluginV4 } from '@/app/infra/entities/plugin'; import { PluginV4 } from '@/app/infra/entities/plugin';
import { getCloudServiceClientSync } from '@/app/infra/http'; import { getCloudServiceClientSync } from '@/app/infra/http';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
interface PluginDetailDialogProps { interface PluginDetailDialogProps {
open: boolean; open: boolean;
@@ -104,6 +105,15 @@ export default function PluginDetailDialog({
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
{plugin!.install_count.toLocaleString()} {t('market.downloads')} {plugin!.install_count.toLocaleString()} {t('market.downloads')}
</Badge> </Badge>
{plugin!.components && Object.keys(plugin!.components).length > 0 && (
<PluginComponentList
components={plugin!.components}
showComponentName={true}
showTitle={false}
useBadge={true}
t={t}
/>
)}
{plugin!.repository && ( {plugin!.repository && (
<button <button
onClick={(e) => { onClick={(e) => {

View File

@@ -1,4 +1,7 @@
import { PluginMarketCardVO } from './PluginMarketCardVO'; import { PluginMarketCardVO } from './PluginMarketCardVO';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
export default function PluginMarketCardComponent({ export default function PluginMarketCardComponent({
cardVO, cardVO,
@@ -7,18 +10,32 @@ export default function PluginMarketCardComponent({
cardVO: PluginMarketCardVO; cardVO: PluginMarketCardVO;
onPluginClick?: (author: string, pluginName: string) => void; onPluginClick?: (author: string, pluginName: string) => void;
}) { }) {
const { t } = useTranslation();
function handleCardClick() { function handleCardClick() {
if (onPluginClick) { if (onPluginClick) {
onPluginClick(cardVO.author, cardVO.pluginName); onPluginClick(cardVO.author, cardVO.pluginName);
} }
} }
const kindIconMap: Record<string, React.ReactNode> = {
Tool: <Wrench className="w-4 h-4" />,
EventListener: <AudioWaveform className="w-4 h-4" />,
Command: <Hash className="w-4 h-4" />,
};
const componentKindNameMap: Record<string, string> = {
Tool: t('plugins.componentName.Tool'),
EventListener: t('plugins.componentName.EventListener'),
Command: t('plugins.componentName.Command'),
};
return ( return (
<div <div
className="w-[100%] h-auto min-h-[8rem] sm:h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] cursor-pointer hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22]" className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] cursor-pointer hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22]"
onClick={handleCardClick} onClick={handleCardClick}
> >
<div className="w-full h-full flex flex-col justify-between gap-2"> <div className="w-full h-full flex flex-col justify-between gap-3">
{/* 上部分:插件信息 */} {/* 上部分:插件信息 */}
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0"> <div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0">
<img <img
@@ -60,23 +77,45 @@ export default function PluginMarketCardComponent({
</div> </div>
</div> </div>
{/* 下部分:下载量 */} {/* 下部分:下载量和组件列表 */}
<div className="w-full flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0"> <div className="w-full flex flex-row items-center justify-between gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
<svg <div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0" <svg
xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" fill="none"
strokeWidth="2" stroke="currentColor"
> strokeWidth="2"
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> >
<polyline points="7,10 12,15 17,10" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<line x1="12" y1="15" x2="12" y2="3" /> <polyline points="7,10 12,15 17,10" />
</svg> <line x1="12" y1="15" x2="12" y2="3" />
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap"> </svg>
{cardVO.installCount.toLocaleString()} <div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
{cardVO.installCount.toLocaleString()}
</div>
</div> </div>
{/* 组件列表 */}
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
<div className="flex flex-row items-center gap-1">
{Object.entries(cardVO.components).map(([kind, count]) => (
<Badge
key={kind}
variant="outline"
className="flex items-center gap-1"
>
{kindIconMap[kind]}
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
<span className="hidden md:inline">
{componentKindNameMap[kind]}
</span>
<span className="ml-1">{count}</span>
</Badge>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ export interface IPluginMarketCardVO {
iconURL: string; iconURL: string;
githubURL: string; githubURL: string;
version: string; version: string;
components?: Record<string, number>;
} }
export class PluginMarketCardVO implements IPluginMarketCardVO { export class PluginMarketCardVO implements IPluginMarketCardVO {
@@ -20,6 +21,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
githubURL: string; githubURL: string;
installCount: number; installCount: number;
version: string; version: string;
components?: Record<string, number>;
constructor(prop: IPluginMarketCardVO) { constructor(prop: IPluginMarketCardVO) {
this.description = prop.description; this.description = prop.description;
@@ -31,5 +33,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
this.installCount = prop.installCount; this.installCount = prop.installCount;
this.pluginId = prop.pluginId; this.pluginId = prop.pluginId;
this.version = prop.version; this.version = prop.version;
this.components = prop.components;
} }
} }

View File

@@ -40,6 +40,7 @@ export interface PluginV4 {
tags: string[]; tags: string[];
install_count: number; install_count: number;
latest_version: string; latest_version: string;
components: Record<string, number>;
status: PluginV4Status; status: PluginV4Status;
created_at: string; created_at: string;
updated_at: string; updated_at: string;