mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 22:51:13 +00:00
Merge branch 'main' into feat/flash-transfer-thumbnail
This commit is contained in:
commit
1e6468012c
@ -294,8 +294,7 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
|
||||
}
|
||||
|
||||
if (element.markdownElement) {
|
||||
// console.log(element.markdownElement);
|
||||
if (element.markdownElement.mdSummary !== undefined && element.markdownElement.mdExtInfo !== undefined && element.markdownElement.mdExtInfo.flashTransferInfo) {
|
||||
if (element.markdownElement?.mdSummary) {
|
||||
return element.markdownElement.mdSummary;
|
||||
} else {
|
||||
return '[Markdown 消息]';
|
||||
|
||||
@ -561,7 +561,7 @@ export class OneBotMsgApi {
|
||||
|
||||
markdownElement: async (element) => {
|
||||
// 让QQ闪传消息独立出去
|
||||
if (element.mdExtInfo !== undefined && element.mdExtInfo.flashTransferInfo) {
|
||||
if (element?.mdExtInfo?.flashTransferInfo?.filesetId) {
|
||||
return {
|
||||
type: OB11MessageDataType.flashtransfer,
|
||||
data: {
|
||||
|
||||
@ -16,7 +16,8 @@ const getPluginManager = (): OB11PluginMangerAdapter | null => {
|
||||
export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
// 返回成功但带特殊标记
|
||||
return sendSuccess(res, { plugins: [], pluginManagerNotFound: true });
|
||||
}
|
||||
|
||||
// 辅助函数:根据文件名/路径生成唯一ID(作为配置键)
|
||||
@ -113,7 +114,7 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
return sendSuccess(res, allPlugins);
|
||||
return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false });
|
||||
};
|
||||
|
||||
export const ReloadPluginHandler: RequestHandler = async (req, res) => {
|
||||
@ -124,7 +125,7 @@ export const ReloadPluginHandler: RequestHandler = async (req, res) => {
|
||||
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
return sendError(res, '插件管理器未加载,请检查 plugins 目录是否存在');
|
||||
}
|
||||
|
||||
const success = await pluginManager.reloadPlugin(name);
|
||||
|
||||
177
packages/napcat-webui-backend/src/api/PluginStore.ts
Normal file
177
packages/napcat-webui-backend/src/api/PluginStore.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
import { PluginStoreList } from '@/napcat-webui-backend/src/types/PluginStore';
|
||||
|
||||
// Mock数据 - 模拟远程插件列表
|
||||
const mockPluginStoreData: PluginStoreList = {
|
||||
version: '1.0.0',
|
||||
updateTime: new Date().toISOString(),
|
||||
plugins: [
|
||||
{
|
||||
id: 'napcat-plugin-example',
|
||||
name: '示例插件',
|
||||
version: '1.0.0',
|
||||
description: '这是一个示例插件,展示如何开发NapCat插件',
|
||||
author: 'NapCat Team',
|
||||
homepage: 'https://github.com/NapNeko/NapCatQQ',
|
||||
repository: 'https://github.com/NapNeko/NapCatQQ',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-example-1.0.0.zip',
|
||||
tags: ['示例', '教程'],
|
||||
screenshots: ['https://picsum.photos/800/600?random=1'],
|
||||
minNapCatVersion: '1.0.0',
|
||||
downloads: 1234,
|
||||
rating: 4.5,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-20T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'napcat-plugin-auto-reply',
|
||||
name: '自动回复插件',
|
||||
version: '2.1.0',
|
||||
description: '支持关键词匹配的自动回复功能,可配置多种回复规则',
|
||||
author: 'Community',
|
||||
homepage: 'https://github.com/example/auto-reply',
|
||||
repository: 'https://github.com/example/auto-reply',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-auto-reply-2.1.0.zip',
|
||||
tags: ['自动回复', '消息处理'],
|
||||
minNapCatVersion: '1.0.0',
|
||||
downloads: 5678,
|
||||
rating: 4.8,
|
||||
createdAt: '2024-01-05T00:00:00Z',
|
||||
updatedAt: '2024-01-22T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'napcat-plugin-welcome',
|
||||
name: '入群欢迎插件',
|
||||
version: '1.2.3',
|
||||
description: '新成员入群时自动发送欢迎消息,支持自定义欢迎语',
|
||||
author: 'Developer',
|
||||
homepage: 'https://github.com/example/welcome',
|
||||
repository: 'https://github.com/example/welcome',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-welcome-1.2.3.zip',
|
||||
tags: ['欢迎', '群管理'],
|
||||
minNapCatVersion: '1.0.0',
|
||||
downloads: 3456,
|
||||
rating: 4.3,
|
||||
createdAt: '2024-01-10T00:00:00Z',
|
||||
updatedAt: '2024-01-18T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'napcat-plugin-music',
|
||||
name: '音乐点歌插件',
|
||||
version: '3.0.1',
|
||||
description: '支持网易云、QQ音乐等平台的点歌功能',
|
||||
author: 'Music Lover',
|
||||
homepage: 'https://github.com/example/music',
|
||||
repository: 'https://github.com/example/music',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-music-3.0.1.zip',
|
||||
tags: ['音乐', '娱乐'],
|
||||
screenshots: ['https://picsum.photos/800/600?random=4', 'https://picsum.photos/800/600?random=5'],
|
||||
minNapCatVersion: '1.1.0',
|
||||
downloads: 8901,
|
||||
rating: 4.9,
|
||||
createdAt: '2023-12-01T00:00:00Z',
|
||||
updatedAt: '2024-01-23T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'napcat-plugin-admin',
|
||||
name: '群管理插件',
|
||||
version: '2.5.0',
|
||||
description: '提供踢人、禁言、设置管理员等群管理功能',
|
||||
author: 'Admin Tools',
|
||||
homepage: 'https://github.com/example/admin',
|
||||
repository: 'https://github.com/example/admin',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-admin-2.5.0.zip',
|
||||
tags: ['管理', '群管理', '工具'],
|
||||
minNapCatVersion: '1.0.0',
|
||||
downloads: 6789,
|
||||
rating: 4.6,
|
||||
createdAt: '2023-12-15T00:00:00Z',
|
||||
updatedAt: '2024-01-21T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'napcat-plugin-image-search',
|
||||
name: '以图搜图插件',
|
||||
version: '1.5.2',
|
||||
description: '支持多个搜图引擎,快速找到图片来源',
|
||||
author: 'Image Hunter',
|
||||
homepage: 'https://github.com/example/image-search',
|
||||
repository: 'https://github.com/example/image-search',
|
||||
downloadUrl: 'https://example.com/plugins/napcat-plugin-image-search-1.5.2.zip',
|
||||
tags: ['图片', '搜索', '工具'],
|
||||
minNapCatVersion: '1.0.0',
|
||||
downloads: 4567,
|
||||
rating: 4.4,
|
||||
createdAt: '2024-01-08T00:00:00Z',
|
||||
updatedAt: '2024-01-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取插件商店列表
|
||||
* 未来可以从远程URL读取
|
||||
*/
|
||||
export const GetPluginStoreListHandler: RequestHandler = async (_req, res) => {
|
||||
try {
|
||||
// TODO: 未来从远程URL读取
|
||||
// const remoteUrl = 'https://napcat.example.com/plugin-list.json';
|
||||
// const response = await fetch(remoteUrl);
|
||||
// const data = await response.json();
|
||||
|
||||
// 目前返回Mock数据
|
||||
return sendSuccess(res, mockPluginStoreData);
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to fetch plugin store list: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个插件详情
|
||||
*/
|
||||
export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const plugin = mockPluginStoreData.plugins.find(p => p.id === id);
|
||||
|
||||
if (!plugin) {
|
||||
return sendError(res, 'Plugin not found');
|
||||
}
|
||||
|
||||
return sendSuccess(res, plugin);
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to fetch plugin detail: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 安装插件(从商店)
|
||||
* TODO: 实现实际的下载和安装逻辑
|
||||
*/
|
||||
export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.body;
|
||||
|
||||
if (!id) {
|
||||
return sendError(res, 'Plugin ID is required');
|
||||
}
|
||||
|
||||
const plugin = mockPluginStoreData.plugins.find(p => p.id === id);
|
||||
|
||||
if (!plugin) {
|
||||
return sendError(res, 'Plugin not found in store');
|
||||
}
|
||||
|
||||
// TODO: 实现实际的下载和安装逻辑
|
||||
// 1. 下载插件文件
|
||||
// 2. 解压到插件目录
|
||||
// 3. 加载插件
|
||||
|
||||
return sendSuccess(res, {
|
||||
message: 'Plugin installation started',
|
||||
plugin: plugin
|
||||
});
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to install plugin: ' + e.message);
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { GetPluginListHandler, ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler } from '@/napcat-webui-backend/src/api/Plugin';
|
||||
import { GetPluginStoreListHandler, GetPluginStoreDetailHandler, InstallPluginFromStoreHandler } from '@/napcat-webui-backend/src/api/PluginStore';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
@ -8,4 +9,9 @@ router.post('/Reload', ReloadPluginHandler);
|
||||
router.post('/SetStatus', SetPluginStatusHandler);
|
||||
router.post('/Uninstall', UninstallPluginHandler);
|
||||
|
||||
// 插件商店相关路由
|
||||
router.get('/Store/List', GetPluginStoreListHandler);
|
||||
router.get('/Store/Detail/:id', GetPluginStoreDetailHandler);
|
||||
router.post('/Store/Install', InstallPluginFromStoreHandler);
|
||||
|
||||
export { router as PluginRouter };
|
||||
|
||||
27
packages/napcat-webui-backend/src/types/PluginStore.ts
Normal file
27
packages/napcat-webui-backend/src/types/PluginStore.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// 插件商店相关类型定义
|
||||
|
||||
export interface PluginStoreItem {
|
||||
id: string; // 插件唯一标识
|
||||
name: string; // 插件名称
|
||||
version: string; // 最新版本
|
||||
description: string; // 插件描述
|
||||
author: string; // 作者
|
||||
homepage?: string; // 主页链接
|
||||
repository?: string; // 仓库地址
|
||||
downloadUrl: string; // 下载地址
|
||||
tags?: string[]; // 标签
|
||||
icon?: string; // 图标URL
|
||||
screenshots?: string[]; // 截图
|
||||
minNapCatVersion?: string; // 最低NapCat版本要求
|
||||
dependencies?: Record<string, string>; // 依赖
|
||||
downloads?: number; // 下载次数
|
||||
rating?: number; // 评分
|
||||
createdAt?: string; // 创建时间
|
||||
updatedAt?: string; // 更新时间
|
||||
}
|
||||
|
||||
export interface PluginStoreList {
|
||||
version: string; // 索引版本
|
||||
updateTime: string; // 更新时间
|
||||
plugins: PluginStoreItem[]; // 插件列表
|
||||
}
|
||||
@ -26,6 +26,7 @@ const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
|
||||
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'));
|
||||
|
||||
function App () {
|
||||
return (
|
||||
@ -78,6 +79,7 @@ function AppRoutes () {
|
||||
<Route path='file_manager' element={<FileManagerPage />} />
|
||||
<Route path='terminal' element={<TerminalPage />} />
|
||||
<Route path='plugins' element={<PluginPage />} />
|
||||
<Route path='plugin_store' element={<PluginStorePage />} />
|
||||
<Route path='about' element={<AboutPage />} />
|
||||
</Route>
|
||||
<Route path='/qq_login' element={<QQLoginPage />} />
|
||||
|
||||
@ -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;
|
||||
@ -55,7 +55,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']> (
|
||||
|
||||
if (['httpServers', 'httpSseServers', 'websocketServers'].includes(field)) {
|
||||
const serverData = data as any;
|
||||
if (!serverData.token) {
|
||||
if (!serverData.token && serverData.host !== '127.0.0.1') {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '安全警告',
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
LuTerminal,
|
||||
LuZap,
|
||||
LuPackage,
|
||||
LuStore,
|
||||
} from 'react-icons/lu';
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
@ -65,6 +66,11 @@ export const siteConfig = {
|
||||
icon: <LuPackage className='w-5 h-5' />,
|
||||
href: '/plugins',
|
||||
},
|
||||
{
|
||||
label: '插件商店',
|
||||
icon: <LuStore className='w-5 h-5' />,
|
||||
href: '/plugin_store',
|
||||
},
|
||||
{
|
||||
label: '系统终端',
|
||||
icon: <LuTerminal className='w-5 h-5' />,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { serverRequest } from '@/utils/request';
|
||||
import { PluginStoreList, PluginStoreItem } from '@/types/plugin-store';
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
@ -9,6 +10,11 @@ export interface PluginItem {
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface PluginListResponse {
|
||||
plugins: PluginItem[];
|
||||
pluginManagerNotFound: boolean;
|
||||
}
|
||||
|
||||
export interface ServerResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -17,7 +23,7 @@ export interface ServerResponse<T> {
|
||||
|
||||
export default class PluginManager {
|
||||
public static async getPluginList () {
|
||||
const { data } = await serverRequest.get<ServerResponse<PluginItem[]>>('/Plugin/List');
|
||||
const { data } = await serverRequest.get<ServerResponse<PluginListResponse>>('/Plugin/List');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
@ -32,4 +38,19 @@ export default class PluginManager {
|
||||
public static async uninstallPlugin (name: string, filename?: string) {
|
||||
await serverRequest.post<ServerResponse<void>>('/Plugin/Uninstall', { name, filename });
|
||||
}
|
||||
|
||||
// 插件商店相关方法
|
||||
public static async getPluginStoreList () {
|
||||
const { data } = await serverRequest.get<ServerResponse<PluginStoreList>>('/Plugin/Store/List');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public static async getPluginStoreDetail (id: string) {
|
||||
const { data } = await serverRequest.get<ServerResponse<PluginStoreItem>>(`/Plugin/Store/Detail/${id}`);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public static async installPluginFromStore (id: string) {
|
||||
await serverRequest.post<ServerResponse<void>>('/Plugin/Store/Install', { id });
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,13 +11,20 @@ import useDialog from '@/hooks/use-dialog';
|
||||
export default function PluginPage () {
|
||||
const [plugins, setPlugins] = useState<PluginItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pluginManagerNotFound, setPluginManagerNotFound] = useState(false);
|
||||
const dialog = useDialog();
|
||||
|
||||
const loadPlugins = async () => {
|
||||
setLoading(true);
|
||||
setPluginManagerNotFound(false);
|
||||
try {
|
||||
const data = await PluginManager.getPluginList();
|
||||
setPlugins(data);
|
||||
const result = await PluginManager.getPluginList();
|
||||
if (result.pluginManagerNotFound) {
|
||||
setPluginManagerNotFound(true);
|
||||
setPlugins([]);
|
||||
} else {
|
||||
setPlugins(result.plugins);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
@ -94,7 +101,17 @@ export default function PluginPage () {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{plugins.length === 0 ? (
|
||||
{pluginManagerNotFound ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-center">
|
||||
<div className="text-6xl mb-4">📦</div>
|
||||
<h2 className="text-xl font-semibold text-default-700 dark:text-white/90 mb-2">
|
||||
无插件加载
|
||||
</h2>
|
||||
<p className="text-default-500 dark:text-white/60 max-w-md">
|
||||
插件管理器未加载,请检查 plugins 目录是否存在
|
||||
</p>
|
||||
</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'>
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import { Tab, Tabs } from '@heroui/tabs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoMdRefresh, IoMdSearch } from 'react-icons/io';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import PluginStoreCard from '@/components/display_card/plugin_store_card';
|
||||
import PluginManager from '@/controllers/plugin_manager';
|
||||
import { PluginStoreItem } from '@/types/plugin-store';
|
||||
|
||||
interface EmptySectionProps {
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
const EmptySection: React.FC<EmptySectionProps> = ({ isEmpty }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('text-default-400', {
|
||||
hidden: !isEmpty,
|
||||
})}
|
||||
>
|
||||
暂时没有可用的插件
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PluginStorePage () {
|
||||
const [plugins, setPlugins] = useState<PluginStoreItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<string>('all');
|
||||
|
||||
const loadPlugins = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await PluginManager.getPluginStoreList();
|
||||
setPlugins(data.plugins);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPlugins();
|
||||
}, []);
|
||||
|
||||
// 按标签分类和搜索
|
||||
const categorizedPlugins = useMemo(() => {
|
||||
let filtered = plugins;
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery) {
|
||||
filtered = filtered.filter(
|
||||
(p) =>
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.author.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.tags?.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
// 按下载量排序
|
||||
filtered.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
||||
|
||||
// 定义主要分类
|
||||
const categories: Record<string, PluginStoreItem[]> = {
|
||||
all: filtered,
|
||||
popular: filtered.filter(p => (p.downloads || 0) > 5000),
|
||||
tools: filtered.filter(p => p.tags?.some(t => ['工具', '管理', '群管理'].includes(t))),
|
||||
entertainment: filtered.filter(p => p.tags?.some(t => ['娱乐', '音乐', '游戏'].includes(t))),
|
||||
message: filtered.filter(p => p.tags?.some(t => ['消息处理', '自动回复', '欢迎'].includes(t))),
|
||||
};
|
||||
|
||||
return categories;
|
||||
}, [plugins, searchQuery]);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
{ key: 'all', title: '全部', count: categorizedPlugins.all?.length || 0 },
|
||||
{ key: 'popular', title: '热门推荐', count: categorizedPlugins.popular?.length || 0 },
|
||||
{ key: 'tools', title: '工具管理', count: categorizedPlugins.tools?.length || 0 },
|
||||
{ key: 'entertainment', title: '娱乐功能', count: categorizedPlugins.entertainment?.length || 0 },
|
||||
{ key: 'message', title: '消息处理', count: categorizedPlugins.message?.length || 0 },
|
||||
];
|
||||
}, [categorizedPlugins]);
|
||||
|
||||
const handleInstall = async () => {
|
||||
toast('该功能尚未完工,敬请期待', {
|
||||
icon: '🚧',
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
placeholder="搜索插件名称、描述、作者或标签..."
|
||||
startContent={<IoMdSearch className="text-default-400" />}
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 标签页 */}
|
||||
<Tabs
|
||||
aria-label="Plugin Store Categories"
|
||||
className="max-w-full"
|
||||
selectedKey={activeTab}
|
||||
onSelectionChange={(key) => setActiveTab(String(key))}
|
||||
classNames={{
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
title={`${tab.title} (${tab.count})`}
|
||||
>
|
||||
<EmptySection isEmpty={!categorizedPlugins[tab.key]?.length} />
|
||||
<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">
|
||||
{categorizedPlugins[tab.key]?.map((plugin) => (
|
||||
<PluginStoreCard
|
||||
key={plugin.id}
|
||||
data={plugin}
|
||||
onInstall={handleInstall}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
packages/napcat-webui-frontend/src/types/plugin-store.ts
Normal file
27
packages/napcat-webui-frontend/src/types/plugin-store.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// 插件商店相关类型定义
|
||||
|
||||
export interface PluginStoreItem {
|
||||
id: string; // 插件唯一标识
|
||||
name: string; // 插件名称
|
||||
version: string; // 最新版本
|
||||
description: string; // 插件描述
|
||||
author: string; // 作者
|
||||
homepage?: string; // 主页链接
|
||||
repository?: string; // 仓库地址
|
||||
downloadUrl: string; // 下载地址
|
||||
tags?: string[]; // 标签
|
||||
icon?: string; // 图标URL
|
||||
screenshots?: string[]; // 截图
|
||||
minNapCatVersion?: string; // 最低NapCat版本要求
|
||||
dependencies?: Record<string, string>; // 依赖
|
||||
downloads?: number; // 下载次数
|
||||
rating?: number; // 评分
|
||||
createdAt?: string; // 创建时间
|
||||
updatedAt?: string; // 更新时间
|
||||
}
|
||||
|
||||
export interface PluginStoreList {
|
||||
version: string; // 索引版本
|
||||
updateTime: string; // 更新时间
|
||||
plugins: PluginStoreItem[]; // 插件列表
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user