Add plugin management to WebUI backend and frontend

Implemented backend API and router for plugin management (list, reload, enable/disable, uninstall) and exposed corresponding frontend controller and dashboard page. Updated navigation and site config to include plugin management. Refactored plugin manager adapter for public methods and improved plugin metadata handling.
This commit is contained in:
手瓜一十雪
2026-01-17 16:14:46 +08:00
parent 5d99d906e6
commit 559c14b702
11 changed files with 504 additions and 5 deletions

View File

@@ -0,0 +1,116 @@
import { Button } from '@heroui/button';
import { Switch } from '@heroui/switch';
import clsx from 'clsx';
import { useState } from 'react';
import { MdDeleteForever, MdPublishedWithChanges } from 'react-icons/md';
import DisplayCardContainer from './container';
import { PluginItem } from '@/controllers/plugin_manager';
export interface PluginDisplayCardProps {
data: PluginItem;
onReload: () => Promise<void>;
onToggleStatus: () => Promise<void>;
onUninstall: () => Promise<void>;
}
const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
data,
onReload,
onToggleStatus,
onUninstall,
}) => {
const { name, version, author, description, status } = data;
const isEnabled = status === 'active';
const [processing, setProcessing] = useState(false);
const handleToggle = () => {
setProcessing(true);
onToggleStatus().finally(() => setProcessing(false));
};
const handleReload = () => {
setProcessing(true);
onReload().finally(() => setProcessing(false));
};
const handleUninstall = () => {
setProcessing(true);
onUninstall().finally(() => setProcessing(false));
};
return (
<DisplayCardContainer
className='w-full max-w-[420px]'
action={
<div className='flex gap-2 w-full'>
<Button
fullWidth
radius='full'
size='sm'
variant='flat'
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-primary/20 hover:text-primary transition-colors'
startContent={<MdPublishedWithChanges size={16} />}
onPress={handleReload}
isDisabled={!isEnabled || processing}
>
</Button>
<Button
fullWidth
radius='full'
size='sm'
variant='flat'
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-danger/20 hover:text-danger transition-colors'
startContent={<MdDeleteForever size={16} />}
onPress={handleUninstall}
isDisabled={processing}
>
</Button>
</div>
}
enableSwitch={
<Switch
isDisabled={processing}
isSelected={isEnabled}
onChange={handleToggle}
classNames={{
wrapper: 'group-data-[selected=true]:bg-primary-400',
}}
/>
}
title={name}
>
<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'>
{version}
</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'>
{author || '未知'}
</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'>
{description || '暂无描述'}
</div>
</div>
</div>
</DisplayCardContainer>
);
};
export default PluginDisplayCard;