mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 21:10:23 +00:00
Add plugin WebUI extension page and API routing support
Introduces a plugin router registry for registering plugin-specific API routes, static resources, and extension pages. Updates the plugin manager and context to expose the router, and implements backend and frontend support for serving and displaying plugin extension pages in the WebUI. Also adds a demo extension page and static resource to the builtin plugin.
This commit is contained in:
@@ -27,6 +27,7 @@ const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
|
||||
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
|
||||
const PluginPage = lazy(() => import('@/pages/dashboard/plugin'));
|
||||
const PluginStorePage = lazy(() => import('@/pages/dashboard/plugin_store'));
|
||||
const ExtensionPage = lazy(() => import('@/pages/dashboard/extension'));
|
||||
|
||||
function App () {
|
||||
return (
|
||||
@@ -80,6 +81,7 @@ function AppRoutes () {
|
||||
<Route path='terminal' element={<TerminalPage />} />
|
||||
<Route path='plugins' element={<PluginPage />} />
|
||||
<Route path='plugin_store' element={<PluginStorePage />} />
|
||||
<Route path='extension' element={<ExtensionPage />} />
|
||||
<Route path='about' element={<AboutPage />} />
|
||||
</Route>
|
||||
<Route path='/qq_login' element={<QQLoginPage />} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
LuZap,
|
||||
LuPackage,
|
||||
LuStore,
|
||||
LuPuzzle,
|
||||
} from 'react-icons/lu';
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
@@ -66,6 +67,11 @@ export const siteConfig = {
|
||||
icon: <LuStore className='w-5 h-5' />,
|
||||
href: '/plugin_store',
|
||||
},
|
||||
{
|
||||
label: '扩展页面',
|
||||
icon: <LuPuzzle className='w-5 h-5' />,
|
||||
href: '/extension',
|
||||
},
|
||||
{
|
||||
label: '系统终端',
|
||||
icon: <LuTerminal className='w-5 h-5' />,
|
||||
|
||||
@@ -20,12 +20,31 @@ export interface PluginItem {
|
||||
status: PluginStatus;
|
||||
/** 是否有配置项 */
|
||||
hasConfig?: boolean;
|
||||
/** 是否有扩展页面 */
|
||||
hasPages?: boolean;
|
||||
}
|
||||
|
||||
/** 扩展页面信息 */
|
||||
export interface ExtensionPageItem {
|
||||
/** 插件 ID */
|
||||
pluginId: string;
|
||||
/** 插件名称 */
|
||||
pluginName: string;
|
||||
/** 页面路径 */
|
||||
path: string;
|
||||
/** 页面标题 */
|
||||
title: string;
|
||||
/** 页面图标 */
|
||||
icon?: string;
|
||||
/** 页面描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 插件列表响应 */
|
||||
export interface PluginListResponse {
|
||||
plugins: PluginItem[];
|
||||
pluginManagerNotFound: boolean;
|
||||
extensionPages: ExtensionPageItem[];
|
||||
}
|
||||
|
||||
/** 插件配置项定义 */
|
||||
|
||||
162
packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx
Normal file
162
packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Tab, Tabs } from '@heroui/tabs';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoMdRefresh } from 'react-icons/io';
|
||||
import { MdExtension } from 'react-icons/md';
|
||||
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import pluginManager from '@/controllers/plugin_manager';
|
||||
|
||||
interface ExtensionPage {
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
path: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function ExtensionPage () {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [extensionPages, setExtensionPages] = useState<ExtensionPage[]>([]);
|
||||
const [selectedTab, setSelectedTab] = useState<string>('');
|
||||
const [iframeLoading, setIframeLoading] = useState(false);
|
||||
|
||||
const fetchExtensionPages = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await pluginManager.getPluginList();
|
||||
if (result.pluginManagerNotFound) {
|
||||
setExtensionPages([]);
|
||||
} else {
|
||||
setExtensionPages(result.extensionPages || []);
|
||||
// 默认选中第一个
|
||||
if (result.extensionPages?.length > 0 && !selectedTab) {
|
||||
setSelectedTab(`${result.extensionPages[0].pluginId}:${result.extensionPages[0].path}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取扩展页面失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await fetchExtensionPages();
|
||||
};
|
||||
|
||||
// 生成 tabs
|
||||
const tabs = useMemo(() => {
|
||||
return extensionPages.map(page => ({
|
||||
key: `${page.pluginId}:${page.path}`,
|
||||
title: page.title,
|
||||
pluginId: page.pluginId,
|
||||
pluginName: page.pluginName,
|
||||
path: page.path,
|
||||
icon: page.icon,
|
||||
description: page.description
|
||||
}));
|
||||
}, [extensionPages]);
|
||||
|
||||
// 获取当前选中页面的 iframe URL
|
||||
const currentPageUrl = useMemo(() => {
|
||||
if (!selectedTab) return '';
|
||||
const [pluginId, ...pathParts] = selectedTab.split(':');
|
||||
const path = pathParts.join(':').replace(/^\//, '');
|
||||
// 获取认证 token
|
||||
const token = localStorage.getItem('token') || '';
|
||||
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${encodeURIComponent(token)}`;
|
||||
}, [selectedTab]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExtensionPages();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPageUrl) {
|
||||
setIframeLoading(true);
|
||||
}
|
||||
}, [currentPageUrl]);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
setIframeLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>扩展页面 - NapCat WebUI</title>
|
||||
<div className="p-2 md:p-4 relative h-full flex flex-col">
|
||||
<PageLoading loading={loading} />
|
||||
|
||||
<div className="flex mb-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-default-600">
|
||||
<MdExtension size={24} />
|
||||
<span className="text-lg font-medium">插件扩展页面</span>
|
||||
</div>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
|
||||
radius="full"
|
||||
onPress={refresh}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{extensionPages.length === 0 && !loading ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-default-400">
|
||||
<MdExtension size={64} className="mb-4 opacity-50" />
|
||||
<p className="text-lg">暂无插件扩展页面</p>
|
||||
<p className="text-sm mt-2">插件可以通过注册页面来扩展 WebUI 功能</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<Tabs
|
||||
aria-label="Extension Pages"
|
||||
className="max-w-full"
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={(key) => setSelectedTab(key as string)}
|
||||
classNames={{
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-wrap',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
panel: 'flex-1 min-h-0 p-0'
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
{tab.icon && <span>{tab.icon}</span>}
|
||||
<span>{tab.title}</span>
|
||||
<span className="text-xs text-default-400">({tab.pluginName})</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative w-full h-[calc(100vh-220px)] bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden">
|
||||
{iframeLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-default-100/50 z-10">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
src={currentPageUrl}
|
||||
className="w-full h-full border-0"
|
||||
onLoad={handleIframeLoad}
|
||||
title={tab.title}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user