Add plugin store feature to backend and frontend

Implemented plugin store API endpoints and types in the backend, including mock data and handlers for listing, detail, and install actions. Added plugin store page, card component, and related logic to the frontend, with navigation and categorized browsing. Updated plugin manager controller and site config to support the new plugin store functionality.
This commit is contained in:
手瓜一十雪
2026-01-24 12:00:26 +08:00
parent e239662514
commit dc858807ae
11 changed files with 569 additions and 7 deletions

View File

@@ -0,0 +1,117 @@
import { Button } from '@heroui/button';
import { Chip } from '@heroui/chip';
import { useState } from 'react';
import { IoMdStar, IoMdDownload } from 'react-icons/io';
import DisplayCardContainer from './container';
import { PluginStoreItem } from '@/types/plugin-store';
export interface PluginStoreCardProps {
data: PluginStoreItem;
onInstall: () => Promise<void>;
}
const PluginStoreCard: React.FC<PluginStoreCardProps> = ({
data,
onInstall,
}) => {
const { name, version, author, description, tags, rating, icon } = data;
const [processing, setProcessing] = useState(false);
const handleInstall = () => {
setProcessing(true);
onInstall().finally(() => setProcessing(false));
};
return (
<DisplayCardContainer
className='w-full max-w-[420px]'
title={name}
tag={
<Chip
className="ml-auto"
color="primary"
size="sm"
variant="flat"
>
v{version}
</Chip>
}
enableSwitch={
icon ? (
<div className="flex items-center gap-2">
<img
src={icon}
alt={name}
className="w-10 h-10 rounded-lg object-cover"
/>
</div>
) : undefined
}
action={
<Button
fullWidth
radius='full'
size='sm'
color='primary'
startContent={<IoMdDownload size={16} />}
onPress={handleInstall}
isLoading={processing}
isDisabled={processing}
>
</Button>
}
>
<div className='grid grid-cols-2 gap-3'>
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{author || '未知'}
</div>
</div>
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
v{version}
</div>
</div>
<div className='col-span-2 flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 break-words line-clamp-2 h-10 overflow-hidden'>
{description || '暂无描述'}
</div>
</div>
{rating && (
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='flex items-center gap-1 text-sm font-medium text-default-700 dark:text-white/90'>
<IoMdStar className='text-warning' size={16} />
<span>{rating.toFixed(1)}</span>
</div>
</div>
)}
{tags && tags.length > 0 && (
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{tags.slice(0, 2).join(' · ')}
</div>
</div>
)}
</div>
</DisplayCardContainer>
);
};
export default PluginStoreCard;