import { Tab, Tabs } from '@heroui/tabs'; import { Button } from '@heroui/button'; import { Spinner } from '@heroui/spinner'; import { useEffect, useState, useMemo, useRef, useCallback } 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; } // eslint-disable-next-line @typescript-eslint/no-redeclare export default function ExtensionPage () { const [loading, setLoading] = useState(true); const [extensionPages, setExtensionPages] = useState([]); const [selectedTab, setSelectedTab] = useState(''); 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 // 新路由格式不需要鉴权: /plugin/:pluginId/page/:pagePath const currentPageUrl = useMemo(() => { if (!selectedTab) return ''; const [pluginId, ...pathParts] = selectedTab.split(':'); const path = pathParts.join(':').replace(/^\//, ''); return `/plugin/${pluginId}/page/${path}`; }, [selectedTab]); useEffect(() => { fetchExtensionPages(); }, []); useEffect(() => { if (currentPageUrl) { setIframeLoading(true); } }, [currentPageUrl]); const handleIframeLoad = () => { setIframeLoading(false); }; // 在新窗口打开页面(新路由不需要鉴权) const openInNewWindow = (pluginId: string, path: string) => { const cleanPath = path.replace(/^\//, ''); const url = `/plugin/${pluginId}/page/${cleanPath}`; window.open(url, '_blank'); }; // 拖拽滚动支持(鼠标 + 触摸) const scrollRef = useRef(null); const isDragging = useRef(false); const startX = useRef(0); const scrollLeft = useRef(0); const handlePointerDown = useCallback((e: React.PointerEvent) => { const el = scrollRef.current; if (!el) return; isDragging.current = true; startX.current = e.clientX; scrollLeft.current = el.scrollLeft; el.setPointerCapture(e.pointerId); el.style.cursor = 'grabbing'; el.style.userSelect = 'none'; }, []); const handlePointerMove = useCallback((e: React.PointerEvent) => { if (!isDragging.current || !scrollRef.current) return; const dx = e.clientX - startX.current; scrollRef.current.scrollLeft = scrollLeft.current - dx; }, []); const handlePointerUp = useCallback((e: React.PointerEvent) => { if (!isDragging.current || !scrollRef.current) return; isDragging.current = false; scrollRef.current.releasePointerCapture(e.pointerId); scrollRef.current.style.cursor = 'grab'; scrollRef.current.style.userSelect = ''; }, []); return ( <> 扩展页面 - NapCat WebUI
插件扩展页面
{extensionPages.length > 0 && (
setSelectedTab(key as string)} classNames={{ tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-nowrap', cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', panel: 'hidden', }} > {tabs.map((tab) => ( {tab.icon && {tab.icon}} { e.stopPropagation(); openInNewWindow(tab.pluginId, tab.path); }} > {tab.title} ({tab.pluginName})
} /> ))}
)}
{extensionPages.length === 0 && !loading ? (

暂无插件扩展页面

插件可以通过注册页面来扩展 WebUI 功能

) : (
{iframeLoading && (
)}