diff --git a/README.md b/README.md index 97157a8..ec2aaf7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -按照“游戏部署”-“Minecraft部署”部署的风格 继续新增以下部署 -在“游戏部署”导航项中新增“更多游戏部署”标签页并按照要求实现对应功能 -请查阅后端这两个文件实现,要求拓展性要好一点,便于后续继续添加游戏部署 -server\src\modules\game\factorio-deployer.ts -server\src\modules\game\tmodloader-server-api.ts \ No newline at end of file +增加后端插件实现 +在前端导航项的“设置”上方加一个“插件”页面并实现功能 +在后端创建一个插件目录主目录 然后每个插件一个单独的文件夹,以文件夹名称来显示插件名称。每个插件在自己的插件目录可以有一个完整的前端html js css 用户只需要通过点击前往即可访问到 \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index ea245f6..7b92e21 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,7 @@ import InstanceManagerPage from '@/pages/InstanceManagerPage' import GameDeploymentPage from './pages/GameDeploymentPage' import ScheduledTasksPage from '@/pages/ScheduledTasksPage' import SettingsPage from '@/pages/SettingsPage' +import PluginsPage from '@/pages/PluginsPage' import FileManagerPage from '@/pages/FileManagerPage' import AboutProjectPage from '@/pages/AboutProjectPage' import LoadingSpinner from '@/components/LoadingSpinner' @@ -135,6 +136,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 52a2bab..433648b 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -22,7 +22,8 @@ import { Wifi, WifiOff, AlertTriangle, - RefreshCw + RefreshCw, + Puzzle } from 'lucide-react' interface LayoutProps { @@ -47,6 +48,7 @@ const Layout: React.FC = ({ children }) => { { name: '游戏部署', href: '/game-deployment', icon: Download }, { name: '定时任务', href: '/scheduled-tasks', icon: Clock }, { name: '文件管理', href: '/files', icon: FolderOpen }, + { name: '插件', href: '/plugins', icon: Puzzle }, { name: '设置', href: '/settings', icon: Settings }, { name: '关于项目', href: '/about', icon: Info }, ] diff --git a/client/src/pages/PluginsPage.tsx b/client/src/pages/PluginsPage.tsx new file mode 100644 index 0000000..7bb9f7a --- /dev/null +++ b/client/src/pages/PluginsPage.tsx @@ -0,0 +1,574 @@ +import React, { useState, useEffect } from 'react' +import { ApiClient } from '@/utils/api' +import { useNotificationStore } from '@/stores/notificationStore' +import { + Plus, + Settings, + Trash2, + Power, + PowerOff, + ExternalLink, + Edit, + Save, + X, + Puzzle, + User, + Calendar, + Tag, + FileText, + Globe +} from 'lucide-react' + +interface Plugin { + name: string + displayName: string + description: string + version: string + author: string + enabled: boolean + hasWebInterface: boolean + entryPoint?: string + icon?: string + category?: string +} + +interface CreatePluginForm { + name: string + displayName: string + description: string + version: string + author: string + category: string + icon: string +} + +const PluginsPage: React.FC = () => { + const [plugins, setPlugins] = useState([]) + const [loading, setLoading] = useState(true) + const [showCreateModal, setShowCreateModal] = useState(false) + const [editingPlugin, setEditingPlugin] = useState(null) + const [showPluginModal, setShowPluginModal] = useState(false) + const [currentPluginContent, setCurrentPluginContent] = useState('') + const [currentPluginName, setCurrentPluginName] = useState('') + const [createForm, setCreateForm] = useState({ + name: '', + displayName: '', + description: '', + version: '1.0.0', + author: '', + category: '其他', + icon: 'puzzle' + }) + const { addNotification } = useNotificationStore() + const apiClient = new ApiClient() + + const categories = [ + '工具', + '游戏', + '监控', + '管理', + '娱乐', + '开发', + '系统', + '其他' + ] + + const icons = [ + 'puzzle', + 'settings', + 'gamepad-2', + 'monitor', + 'shield', + 'music', + 'code', + 'server', + 'globe', + 'tool', + 'heart', + 'star' + ] + + useEffect(() => { + loadPlugins() + }, []) + + const loadPlugins = async () => { + try { + setLoading(true) + const response = await apiClient.get('/plugins/list') + if (response.success) { + setPlugins(response.data) + } else { + addNotification({ type: 'error', title: '错误', message: '获取插件列表失败' }) + } + } catch (error) { + console.error('获取插件列表失败:', error) + addNotification({ type: 'error', title: '错误', message: '获取插件列表失败' }) + } finally { + setLoading(false) + } + } + + const handleCreatePlugin = async () => { + try { + if (!createForm.name.trim()) { + addNotification({ type: 'error', title: '错误', message: '插件名称不能为空' }) + return + } + + if (!/^[a-zA-Z0-9_-]+$/.test(createForm.name)) { + addNotification({ type: 'error', title: '错误', message: '插件名称只能包含字母、数字、下划线和连字符' }) + return + } + + const response = await apiClient.post('/plugins/create', createForm) + if (response.success) { + addNotification({ type: 'success', title: '成功', message: '插件创建成功' }) + setShowCreateModal(false) + setCreateForm({ + name: '', + displayName: '', + description: '', + version: '1.0.0', + author: '', + category: '其他', + icon: 'puzzle' + }) + loadPlugins() + } else { + addNotification({ type: 'error', title: '错误', message: response.message || '创建插件失败' }) + } + } catch (error) { + console.error('创建插件失败:', error) + addNotification({ type: 'error', title: '错误', message: '创建插件失败' }) + } + } + + const handleTogglePlugin = async (plugin: Plugin) => { + try { + const endpoint = plugin.enabled ? 'disable' : 'enable' + const response = await apiClient.post(`/plugins/${plugin.name}/${endpoint}`) + if (response.success) { + addNotification({ type: 'success', title: '成功', message: `插件已${plugin.enabled ? '禁用' : '启用'}` }) + loadPlugins() + } else { + addNotification({ type: 'error', title: '错误', message: response.message || `${plugin.enabled ? '禁用' : '启用'}插件失败` }) + } + } catch (error) { + console.error('切换插件状态失败:', error) + addNotification({ type: 'error', title: '错误', message: '操作失败' }) + } + } + + const handleDeletePlugin = async (plugin: Plugin) => { + if (!confirm(`确定要删除插件 "${plugin.displayName}" 吗?此操作不可撤销。`)) { + return + } + + try { + const response = await apiClient.delete(`/plugins/${plugin.name}`) + if (response.success) { + addNotification({ type: 'success', title: '成功', message: '插件删除成功' }) + loadPlugins() + } else { + addNotification({ type: 'error', title: '错误', message: response.message || '删除插件失败' }) + } + } catch (error) { + console.error('删除插件失败:', error) + addNotification({ type: 'error', title: '错误', message: '删除插件失败' }) + } + } + + const handleOpenPlugin = async (plugin: Plugin) => { + if (plugin.hasWebInterface && plugin.enabled) { + // 发送正在打开插件的通知 + addNotification({ + type: 'info', + title: '提示', + message: `正在打开插件 ${plugin.displayName || plugin.name}...` + }) + + try { + // 通过API获取插件文件内容 + const response = await apiClient.get(`/plugins/${plugin.name}/files/${plugin.entryPoint || 'index.html'}`) + + // 检查响应数据格式 + if (response.data) { + let content = '' + + // 如果是JSON格式的响应(HTML、CSS、JS文件) + if (typeof response.data === 'object' && response.data.success && response.data.data) { + content = response.data.data + } + // 如果直接返回HTML内容(兼容性处理) + else if (typeof response.data === 'string' && response.data.trim()) { + content = response.data + } + // 如果是JSON格式但失败 + else if (typeof response.data === 'object' && !response.data.success) { + addNotification({ + type: 'error', + title: '错误', + message: response.data.message || '获取插件文件失败' + }) + return + } + + if (content && content.trim()) { + setCurrentPluginContent(content) + setCurrentPluginName(plugin.displayName || plugin.name) + setShowPluginModal(true) + } else { + addNotification({ type: 'error', title: '错误', message: '插件内容为空' }) + } + } else { + addNotification({ type: 'error', title: '错误', message: '无法获取插件内容' }) + } + } catch (error) { + console.error('打开插件失败:', error) + addNotification({ + type: 'error', + title: '错误', + message: error instanceof Error ? error.message : '打开插件失败' + }) + } + } + } + + const getIconComponent = (iconName: string) => { + const iconMap: { [key: string]: React.ComponentType } = { + puzzle: Puzzle, + settings: Settings, + 'gamepad-2': Settings, // 使用Settings作为替代 + monitor: Settings, + shield: Settings, + music: Settings, + code: Settings, + server: Settings, + globe: Globe, + tool: Settings, + heart: Settings, + star: Settings + } + const IconComponent = iconMap[iconName] || Puzzle + return + } + + const getCategoryColor = (category: string) => { + const colorMap: { [key: string]: string } = { + '工具': 'bg-blue-500', + '游戏': 'bg-green-500', + '监控': 'bg-yellow-500', + '管理': 'bg-purple-500', + '娱乐': 'bg-pink-500', + '开发': 'bg-indigo-500', + '系统': 'bg-red-500', + '其他': 'bg-gray-500' + } + return colorMap[category] || 'bg-gray-500' + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* 页面标题和操作 */} +
+
+

插件管理

+

+ 管理和配置系统插件,扩展面板功能 +

+
+ +
+ + {/* 插件列表 */} +
+ {plugins.map((plugin) => ( +
+ {/* 插件头部 */} +
+
+
+ {getIconComponent(plugin.icon || 'puzzle')} +
+
+

+ {plugin.displayName} +

+
+ + {plugin.category || '其他'} + + + v{plugin.version} + +
+
+
+
+ {plugin.enabled ? ( +
+ ) : ( +
+ )} +
+
+ + {/* 插件信息 */} +
+

+ {plugin.description} +

+
+
+ + {plugin.author} +
+ {plugin.hasWebInterface && ( +
+ + Web界面 +
+ )} +
+
+ + {/* 操作按钮 */} +
+
+ + {plugin.hasWebInterface && plugin.enabled && ( + + )} +
+ +
+
+ ))} +
+ + {plugins.length === 0 && ( +
+ +

+ 暂无插件 +

+

+ 创建您的第一个插件来扩展面板功能 +

+ +
+ )} + + {/* 插件展示模态框 */} + {showPluginModal && ( +
+
+
+

{currentPluginName}

+ +
+
+