mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
feat: add plugin components displaying in marketplace page
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ function MarketPageContent({
|
|||||||
),
|
),
|
||||||
githubURL: plugin.repository,
|
githubURL: plugin.repository,
|
||||||
version: plugin.latest_version,
|
version: plugin.latest_version,
|
||||||
|
components: plugin.components,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user