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,115 @@
import { Button } from '@heroui/button';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io';
import PageLoading from '@/components/page_loading';
import PluginDisplayCard from '@/components/display_card/plugin_card';
import PluginManager, { PluginItem } from '@/controllers/plugin_manager';
import useDialog from '@/hooks/use-dialog';
export default function PluginPage () {
const [plugins, setPlugins] = useState<PluginItem[]>([]);
const [loading, setLoading] = useState(false);
const dialog = useDialog();
const loadPlugins = async () => {
setLoading(true);
try {
const data = await PluginManager.getPluginList();
setPlugins(data);
} catch (e: any) {
toast.error(e.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPlugins();
}, []);
const handleReload = async (name: string) => {
const loadingToast = toast.loading('重载中...');
try {
await PluginManager.reloadPlugin(name);
toast.success('重载成功', { id: loadingToast });
loadPlugins();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
}
};
const handleToggle = async (plugin: PluginItem) => {
const isEnable = plugin.status !== 'active';
const actionText = isEnable ? '启用' : '禁用';
const loadingToast = toast.loading(`${actionText}中...`);
try {
await PluginManager.setPluginStatus(plugin.name, isEnable, plugin.filename);
toast.success(`${actionText}成功`, { id: loadingToast });
loadPlugins();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
}
};
const handleUninstall = async (plugin: PluginItem) => {
return new Promise<void>((resolve, reject) => {
dialog.confirm({
title: '卸载插件',
content: `确定要卸载插件「${plugin.name}」吗? 此操作不可恢复。`,
onConfirm: async () => {
const loadingToast = toast.loading('卸载中...');
try {
await PluginManager.uninstallPlugin(plugin.name, plugin.filename);
toast.success('卸载成功', { id: loadingToast });
loadPlugins();
resolve();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
reject(e);
}
},
onCancel: () => {
resolve();
}
});
});
};
return (
<>
<title> - NapCat WebUI</title>
<div className='p-2 md:p-4 relative'>
<PageLoading loading={loading} />
<div className='flex mb-6 items-center gap-4'>
<h1 className="text-2xl font-bold"></h1>
<Button
isIconOnly
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
radius='full'
onPress={loadPlugins}
>
<IoMdRefresh size={24} />
</Button>
</div>
{plugins.length === 0 ? (
<div className="text-default-400"></div>
) : (
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4'>
{plugins.map(plugin => (
<PluginDisplayCard
key={plugin.name}
data={plugin}
onReload={() => handleReload(plugin.name)}
onToggleStatus={() => handleToggle(plugin)}
onUninstall={() => handleUninstall(plugin)}
/>
))}
</div>
)}
</div>
</>
);
}