diff --git a/assets/dashboard.png b/assets/dashboard.png index 09e8924..42a4865 100644 Binary files a/assets/dashboard.png and b/assets/dashboard.png differ diff --git a/assets/dashboard.zh.png b/assets/dashboard.zh.png index ad84131..3edd8a6 100644 Binary files a/assets/dashboard.zh.png and b/assets/dashboard.zh.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b52b461..71eedf7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,398 +1,36 @@ -import { useState, useEffect, useRef, useCallback } from 'react' -import { useTranslation } from 'react-i18next' -import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate } from 'react-router-dom' -import { Server, ApiResponse } from './types' -import ServerCard from './components/ServerCard' -import AddServerForm from './components/AddServerForm' -import EditServerForm from './components/EditServerForm' -import LoginPage from './pages/LoginPage' -import ChangePasswordPage from './pages/ChangePasswordPage' -import ProtectedRoute from './components/ProtectedRoute' -import { AuthProvider, useAuth } from './contexts/AuthContext' - -// 配置选项 -const CONFIG = { - // 初始化启动阶段的配置 - startup: { - maxAttempts: 60, // 初始化阶段最大尝试次数 - pollingInterval: 3000 // 初始阶段轮询间隔 (3秒) - }, - // 正常运行阶段的配置 - normal: { - pollingInterval: 10000 // 正常运行时的轮询间隔 (10秒) - } -} - -// Dashboard component that contains the main application -const Dashboard = () => { - const { t } = useTranslation() - const [servers, setServers] = useState([]) - const [error, setError] = useState(null) - const [refreshKey, setRefreshKey] = useState(0) - const [editingServer, setEditingServer] = useState(null) - const [isInitialLoading, setIsInitialLoading] = useState(true) - const [fetchAttempts, setFetchAttempts] = useState(0) - const { auth, logout } = useAuth() - const navigate = useNavigate() - - // 轮询定时器引用 - const intervalRef = useRef(null) - // 保存当前尝试次数,避免依赖循环 - const attemptsRef = useRef(0) - - // 清理定时器 - const clearTimer = () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - } - - // 开始正常轮询 - const startNormalPolling = useCallback(() => { - // 确保没有其他定时器在运行 - clearTimer() - - const fetchServers = async () => { - try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch('/api/servers', { - headers: { - 'x-auth-token': token || '' - } - }); - const data = await response.json() - - if (data && data.success && Array.isArray(data.data)) { - setServers(data.data) - } else if (data && Array.isArray(data)) { - setServers(data) - } else { - console.error('Invalid server data format:', data) - setServers([]) - } - - // 重置错误状态 - setError(null) - } catch (err) { - console.error('Error fetching servers during normal polling:', err) - - // 使用友好的错误消息 - if (!navigator.onLine) { - setError(t('errors.network')) - } else if (err instanceof TypeError && ( - err.message.includes('NetworkError') || - err.message.includes('Failed to fetch') - )) { - setError(t('errors.serverConnection')) - } else { - setError(t('errors.serverFetch')) - } - } - } - - // 立即执行一次 - fetchServers() - - // 设置定期轮询 - intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval) - }, [t]) - - useEffect(() => { - // 重置尝试计数 - if (refreshKey > 0) { - attemptsRef.current = 0; - setFetchAttempts(0); - } - - // 初始化加载阶段的请求函数 - const fetchInitialData = async () => { - try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch('/api/servers', { - headers: { - 'x-auth-token': token || '' - } - }); - const data = await response.json() - - // 处理API响应中的包装对象,提取data字段 - if (data && data.success && Array.isArray(data.data)) { - setServers(data.data) - setIsInitialLoading(false) - // 初始化成功,开始正常轮询 - startNormalPolling() - return true - } else if (data && Array.isArray(data)) { - // 兼容性处理,如果API直接返回数组 - setServers(data) - setIsInitialLoading(false) - // 初始化成功,开始正常轮询 - startNormalPolling() - return true - } else { - // 如果数据格式不符合预期,设置为空数组 - console.error('Invalid server data format:', data) - setServers([]) - setIsInitialLoading(false) - // 初始化成功但数据为空,开始正常轮询 - startNormalPolling() - return true - } - } catch (err) { - // 增加尝试次数计数,使用 ref 避免触发 effect 重新运行 - attemptsRef.current += 1; - console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err) - - // 更新状态用于显示 - setFetchAttempts(attemptsRef.current) - - // 设置适当的错误消息 - if (!navigator.onLine) { - setError(t('errors.network')) - } else { - setError(t('errors.initialStartup')) - } - - // 如果已超过最大尝试次数,放弃初始化并切换到正常轮询 - if (attemptsRef.current >= CONFIG.startup.maxAttempts) { - console.log('Maximum startup attempts reached, switching to normal polling') - setIsInitialLoading(false) - // 清除初始化的轮询 - clearTimer() - // 切换到正常轮询模式 - startNormalPolling() - } - - return false - } - } - - // 组件挂载时,根据当前状态设置适当的轮询 - if (isInitialLoading) { - // 确保没有其他定时器在运行 - clearTimer() - - // 立即执行一次初始请求 - fetchInitialData() - - // 设置初始阶段的轮询间隔 - intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval) - console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`) - } else { - // 已经初始化完成,开始正常轮询 - startNormalPolling() - } - - // 清理函数 - return () => { - clearTimer() - } - }, [refreshKey, t, isInitialLoading, startNormalPolling]) - - // 手动触发刷新 - const triggerRefresh = () => { - // 清除当前的定时器 - clearTimer() - - // 如果在初始化阶段,重置初始化状态 - if (isInitialLoading) { - setIsInitialLoading(true) - attemptsRef.current = 0 - setFetchAttempts(0) - } - - // refreshKey 的改变会触发 useEffect 再次运行 - setRefreshKey(prevKey => prevKey + 1) - } - - const handleServerAdd = () => { - setRefreshKey(prevKey => prevKey + 1) - } - - const handleServerEdit = (server: Server) => { - // Fetch settings to get the full server config before editing - const token = localStorage.getItem('mcphub_token'); - fetch(`/api/settings`, { - headers: { - 'x-auth-token': token || '' - } - }) - .then(response => response.json()) - .then((settingsData: ApiResponse<{ mcpServers: Record }>) => { - if ( - settingsData && - settingsData.success && - settingsData.data && - settingsData.data.mcpServers && - settingsData.data.mcpServers[server.name] - ) { - const serverConfig = settingsData.data.mcpServers[server.name] - const fullServerData = { - name: server.name, - status: server.status, - tools: server.tools || [], - config: serverConfig, - } - - console.log('Editing server with config:', fullServerData) - setEditingServer(fullServerData) - } else { - console.error('Failed to get server config from settings:', settingsData) - setError(t('server.invalidConfig', { serverName: server.name })) - } - }) - .catch(err => { - console.error('Error fetching server settings:', err) - setError(err instanceof Error ? err.message : String(err)) - }) - } - - const handleEditComplete = () => { - setEditingServer(null) - setRefreshKey(prevKey => prevKey + 1) - } - - const handleServerRemove = async (serverName: string) => { - try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(`/api/servers/${serverName}`, { - method: 'DELETE', - headers: { - 'x-auth-token': token || '' - } - }) - const result = await response.json() - - if (!response.ok) { - setError(result.message || t('server.deleteError', { serverName })) - return - } - - setRefreshKey(prevKey => prevKey + 1) - } catch (err) { - setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err))) - } - } - - const handleServerToggle = async (server: Server, enabled: boolean) => { - try { - const token = localStorage.getItem('mcphub_token'); - const response = await fetch(`/api/servers/${server.name}/toggle`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-auth-token': token || '' - }, - body: JSON.stringify({ enabled }), - }); - - const result = await response.json(); - - if (!response.ok) { - console.error('Failed to toggle server:', result); - setError(t('server.toggleError', { serverName: server.name })); - return; - } - - // Update the UI immediately to reflect the change - setRefreshKey(prevKey => prevKey + 1); - } catch (err) { - console.error('Error toggling server:', err); - setError(err instanceof Error ? err.message : String(err)); - } - }; - - const handleLogout = () => { - logout() - navigate('/login') - } - - return ( -
-
- {error && ( -
-
-
-

{t('app.error')}

-

{error}

-
- -
-
- )} - -
-

{t('app.title')}

-
- - - -
-
- {servers.length === 0 ? ( -
-

{t('app.noServers')}

-
- ) : ( -
- {servers.map((server, index) => ( - - ))} -
- )} - {editingServer && ( - setEditingServer(null)} - /> - )} -
-
- ) -} +import React from 'react'; +import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; +import { AuthProvider } from './contexts/AuthContext'; +import MainLayout from './layouts/MainLayout'; +import ProtectedRoute from './components/ProtectedRoute'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/Dashboard'; +import ServersPage from './pages/ServersPage'; +import SettingsPage from './pages/SettingsPage'; function App() { return ( + {/* 公共路由 */} } /> + + {/* 受保护的路由,使用 MainLayout 作为布局容器 */} }> - } /> - } /> + }> + } /> + } /> + } /> + + + {/* 未匹配的路由重定向到首页 */} } /> - ) + ); } -export default App \ No newline at end of file +export default App; \ No newline at end of file diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx index d9e830f..6cc2a49 100644 --- a/frontend/src/components/ServerCard.tsx +++ b/frontend/src/components/ServerCard.tsx @@ -63,12 +63,6 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => > {t('server.edit')} -
+ diff --git a/frontend/src/components/layout/Content.tsx b/frontend/src/components/layout/Content.tsx new file mode 100644 index 0000000..faf4890 --- /dev/null +++ b/frontend/src/components/layout/Content.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; + +interface ContentProps { + children: ReactNode; +} + +const Content: React.FC = ({ children }) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default Content; \ No newline at end of file diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..103ceba --- /dev/null +++ b/frontend/src/components/layout/Header.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; + +interface HeaderProps { + onToggleSidebar: () => void; +} + +const Header: React.FC = ({ onToggleSidebar }) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { auth, logout } = useAuth(); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + return ( +
+
+
+ {/* 侧边栏切换按钮 */} + + + {/* 应用标题 */} +

{t('app.title')}

+
+ + {/* 用户信息和操作 */} +
+ {auth.user && ( + + {t('app.welcomeUser', { username: auth.user.username })} + + )} + +
+ +
+
+
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..1d227c6 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavLink, useLocation } from 'react-router-dom'; + +interface SidebarProps { + collapsed: boolean; +} + +interface MenuItem { + path: string; + label: string; + icon: React.ReactNode; +} + +const Sidebar: React.FC = ({ collapsed }) => { + const { t } = useTranslation(); + const location = useLocation(); + + // 菜单项配置 + const menuItems: MenuItem[] = [ + { + path: '/', + label: t('nav.dashboard'), + icon: ( + + + + + ), + }, + { + path: '/servers', + label: t('nav.servers'), + icon: ( + + + + ), + }, + { + path: '/settings', + label: t('nav.settings'), + icon: ( + + + + ), + }, + ]; + + return ( + + ); +}; + +export default Sidebar; \ No newline at end of file diff --git a/frontend/src/hooks/useServerData.ts b/frontend/src/hooks/useServerData.ts new file mode 100644 index 0000000..3dad563 --- /dev/null +++ b/frontend/src/hooks/useServerData.ts @@ -0,0 +1,306 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Server, ApiResponse } from '@/types'; + +// 配置选项 +const CONFIG = { + // 初始化启动阶段的配置 + startup: { + maxAttempts: 60, // 初始化阶段最大尝试次数 + pollingInterval: 3000 // 初始阶段轮询间隔 (3秒) + }, + // 正常运行阶段的配置 + normal: { + pollingInterval: 10000 // 正常运行时的轮询间隔 (10秒) + } +}; + +export const useServerData = () => { + const { t } = useTranslation(); + const [servers, setServers] = useState([]); + const [error, setError] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [fetchAttempts, setFetchAttempts] = useState(0); + + // 轮询定时器引用 + const intervalRef = useRef(null); + // 保存当前尝试次数,避免依赖循环 + const attemptsRef = useRef(0); + + // 清理定时器 + const clearTimer = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + // 开始正常轮询 + const startNormalPolling = useCallback(() => { + // 确保没有其他定时器在运行 + clearTimer(); + + const fetchServers = async () => { + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch('/api/servers', { + headers: { + 'x-auth-token': token || '' + } + }); + const data = await response.json(); + + if (data && data.success && Array.isArray(data.data)) { + setServers(data.data); + } else if (data && Array.isArray(data)) { + setServers(data); + } else { + console.error('Invalid server data format:', data); + setServers([]); + } + + // 重置错误状态 + setError(null); + } catch (err) { + console.error('Error fetching servers during normal polling:', err); + + // 使用友好的错误消息 + if (!navigator.onLine) { + setError(t('errors.network')); + } else if (err instanceof TypeError && ( + err.message.includes('NetworkError') || + err.message.includes('Failed to fetch') + )) { + setError(t('errors.serverConnection')); + } else { + setError(t('errors.serverFetch')); + } + } + }; + + // 立即执行一次 + fetchServers(); + + // 设置定期轮询 + intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval); + }, [t]); + + useEffect(() => { + // 重置尝试计数 + if (refreshKey > 0) { + attemptsRef.current = 0; + setFetchAttempts(0); + } + + // 初始化加载阶段的请求函数 + const fetchInitialData = async () => { + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch('/api/servers', { + headers: { + 'x-auth-token': token || '' + } + }); + const data = await response.json(); + + // 处理API响应中的包装对象,提取data字段 + if (data && data.success && Array.isArray(data.data)) { + setServers(data.data); + setIsInitialLoading(false); + // 初始化成功,开始正常轮询 + startNormalPolling(); + return true; + } else if (data && Array.isArray(data)) { + // 兼容性处理,如果API直接返回数组 + setServers(data); + setIsInitialLoading(false); + // 初始化成功,开始正常轮询 + startNormalPolling(); + return true; + } else { + // 如果数据格式不符合预期,设置为空数组 + console.error('Invalid server data format:', data); + setServers([]); + setIsInitialLoading(false); + // 初始化成功但数据为空,开始正常轮询 + startNormalPolling(); + return true; + } + } catch (err) { + // 增加尝试次数计数,使用 ref 避免触发 effect 重新运行 + attemptsRef.current += 1; + console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err); + + // 更新状态用于显示 + setFetchAttempts(attemptsRef.current); + + // 设置适当的错误消息 + if (!navigator.onLine) { + setError(t('errors.network')); + } else { + setError(t('errors.initialStartup')); + } + + // 如果已超过最大尝试次数,放弃初始化并切换到正常轮询 + if (attemptsRef.current >= CONFIG.startup.maxAttempts) { + console.log('Maximum startup attempts reached, switching to normal polling'); + setIsInitialLoading(false); + // 清除初始化的轮询 + clearTimer(); + // 切换到正常轮询模式 + startNormalPolling(); + } + + return false; + } + }; + + // 组件挂载时,根据当前状态设置适当的轮询 + if (isInitialLoading) { + // 确保没有其他定时器在运行 + clearTimer(); + + // 立即执行一次初始请求 + fetchInitialData(); + + // 设置初始阶段的轮询间隔 + intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval); + console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`); + } else { + // 已经初始化完成,开始正常轮询 + startNormalPolling(); + } + + // 清理函数 + return () => { + clearTimer(); + }; + }, [refreshKey, t, isInitialLoading, startNormalPolling]); + + // 手动触发刷新 + const triggerRefresh = () => { + // 清除当前的定时器 + clearTimer(); + + // 如果在初始化阶段,重置初始化状态 + if (isInitialLoading) { + setIsInitialLoading(true); + attemptsRef.current = 0; + setFetchAttempts(0); + } + + // refreshKey 的改变会触发 useEffect 再次运行 + setRefreshKey(prevKey => prevKey + 1); + }; + + // 服务器相关操作 + const handleServerAdd = () => { + setRefreshKey(prevKey => prevKey + 1); + }; + + const handleServerEdit = async (server: Server) => { + try { + // Fetch settings to get the full server config before editing + const token = localStorage.getItem('mcphub_token'); + const response = await fetch(`/api/settings`, { + headers: { + 'x-auth-token': token || '' + } + }); + + const settingsData: ApiResponse<{ mcpServers: Record }> = await response.json(); + + if ( + settingsData && + settingsData.success && + settingsData.data && + settingsData.data.mcpServers && + settingsData.data.mcpServers[server.name] + ) { + const serverConfig = settingsData.data.mcpServers[server.name]; + return { + name: server.name, + status: server.status, + tools: server.tools || [], + config: serverConfig, + }; + } else { + console.error('Failed to get server config from settings:', settingsData); + setError(t('server.invalidConfig', { serverName: server.name })); + return null; + } + } catch (err) { + console.error('Error fetching server settings:', err); + setError(err instanceof Error ? err.message : String(err)); + return null; + } + }; + + const handleServerRemove = async (serverName: string) => { + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch(`/api/servers/${serverName}`, { + method: 'DELETE', + headers: { + 'x-auth-token': token || '' + } + }); + const result = await response.json(); + + if (!response.ok) { + setError(result.message || t('server.deleteError', { serverName })); + return false; + } + + setRefreshKey(prevKey => prevKey + 1); + return true; + } catch (err) { + setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err))); + return false; + } + }; + + const handleServerToggle = async (server: Server, enabled: boolean) => { + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch(`/api/servers/${server.name}/toggle`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token || '' + }, + body: JSON.stringify({ enabled }), + }); + + const result = await response.json(); + + if (!response.ok) { + console.error('Failed to toggle server:', result); + setError(t('server.toggleError', { serverName: server.name })); + return false; + } + + // Update the UI immediately to reflect the change + setRefreshKey(prevKey => prevKey + 1); + return true; + } catch (err) { + console.error('Error toggling server:', err); + setError(err instanceof Error ? err.message : String(err)); + return false; + } + }; + + return { + servers, + error, + setError, + isLoading: isInitialLoading, + fetchAttempts, + triggerRefresh, + handleServerAdd, + handleServerEdit, + handleServerRemove, + handleServerToggle + }; +}; \ No newline at end of file diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..fa1f49d --- /dev/null +++ b/frontend/src/layouts/MainLayout.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import Header from '@/components/layout/Header'; +import Sidebar from '@/components/layout/Sidebar'; +import Content from '@/components/layout/Content'; + +const MainLayout: React.FC = () => { + // 控制侧边栏展开/折叠状态 + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + + const toggleSidebar = () => { + setSidebarCollapsed(!sidebarCollapsed); + }; + + return ( +
+ {/* 顶部导航 */} +
+ +
+ {/* 侧边导航 */} + + + {/* 主内容区域 */} + + + +
+
+ ); +}; + +export default MainLayout; \ No newline at end of file diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index c050998..79e6fe7 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -7,7 +7,9 @@ "loading": "Loading...", "logout": "Logout", "profile": "Profile", - "changePassword": "Change Password" + "changePassword": "Change Password", + "toggleSidebar": "Toggle Sidebar", + "welcomeUser": "Welcome, {{username}}" }, "auth": { "login": "Login", @@ -51,9 +53,14 @@ "envVars": "Environment Variables", "key": "key", "value": "value", + "enabled": "Enabled", "enable": "Enable", "disable": "Disable", - "remove": "Remove" + "remove": "Remove", + "toggleError": "Failed to toggle server {{serverName}}", + "alreadyExists": "Server {{serverName}} already exists", + "invalidData": "Invalid server data provided", + "notFound": "Server {{serverName}} not found" }, "status": { "online": "Online", @@ -72,6 +79,30 @@ "common": { "processing": "Processing...", "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "refresh": "Refresh" + }, + "nav": { + "dashboard": "Dashboard", + "servers": "Servers", + "settings": "Settings", + "changePassword": "Change Password" + }, + "pages": { + "dashboard": { + "title": "Dashboard", + "totalServers": "Total Servers", + "onlineServers": "Online Servers", + "offlineServers": "Offline Servers", + "connectingServers": "Connecting Servers", + "recentServers": "Recent Servers" + }, + "servers": { + "title": "Servers Management" + }, + "settings": { + "title": "Settings", + "language": "Language" + } } } \ No newline at end of file diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 999a710..2a5e158 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -7,7 +7,9 @@ "loading": "加载中...", "logout": "退出登录", "profile": "个人资料", - "changePassword": "修改密码" + "changePassword": "修改密码", + "toggleSidebar": "切换侧边栏", + "welcomeUser": "欢迎, {{username}}" }, "auth": { "login": "登录", @@ -51,9 +53,14 @@ "envVars": "环境变量", "key": "键", "value": "值", + "enabled": "已启用", "enable": "启用", "disable": "禁用", - "remove": "移除" + "remove": "移除", + "toggleError": "切换服务器 {{serverName}} 状态失败", + "alreadyExists": "服务器 {{serverName}} 已经存在", + "invalidData": "提供的服务器数据无效", + "notFound": "找不到服务器 {{serverName}}" }, "status": { "online": "在线", @@ -72,6 +79,30 @@ "common": { "processing": "处理中...", "save": "保存", - "cancel": "取消" + "cancel": "取消", + "refresh": "刷新" + }, + "nav": { + "dashboard": "仪表盘", + "servers": "服务器", + "settings": "设置", + "changePassword": "修改密码" + }, + "pages": { + "dashboard": { + "title": "仪表盘", + "totalServers": "服务器总数", + "onlineServers": "在线服务器", + "offlineServers": "离线服务器", + "connectingServers": "连接中服务", + "recentServers": "最近的服务器" + }, + "servers": { + "title": "服务器管理" + }, + "settings": { + "title": "设置", + "language": "语言" + } } } \ No newline at end of file diff --git a/frontend/src/pages/ChangePasswordPage.tsx b/frontend/src/pages/ChangePasswordPage.tsx deleted file mode 100644 index e9c8716..0000000 --- a/frontend/src/pages/ChangePasswordPage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import ChangePasswordForm from '../components/ChangePasswordForm'; - -const ChangePasswordPage: React.FC = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - const handleSuccess = () => { - setTimeout(() => { - navigate('/'); - }, 2000); - }; - - const handleCancel = () => { - navigate('/'); - }; - - return ( -
-
-

{t('auth.changePassword')}

- -
-
- ); -}; - -export default ChangePasswordPage; \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..1cf3f13 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,206 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useServerData } from '@/hooks/useServerData'; +import { ServerStatus } from '@/types'; + +const DashboardPage: React.FC = () => { + const { t } = useTranslation(); + const { servers, error, setError, isLoading } = useServerData(); + + // 计算服务器统计信息 + const serverStats = { + total: servers.length, + online: servers.filter(server => server.status === 'connected').length, + offline: servers.filter(server => server.status === 'disconnected').length, + connecting: servers.filter(server => server.status === 'connecting').length + }; + + // Map status to translation keys + const statusTranslations = { + connected: 'status.online', + disconnected: 'status.offline', + connecting: 'status.connecting' + } + + // 计算各状态百分比(用于仪表板展示) + const getStatusPercentage = (status: ServerStatus) => { + if (servers.length === 0) return 0; + return Math.round((servers.filter(server => server.status === status).length / servers.length) * 100); + }; + + return ( +
+

{t('pages.dashboard.title')}

+ + {error && ( +
+
+
+

{t('app.error')}

+

{error}

+
+ +
+
+ )} + + {isLoading ? ( +
+
+ + + + +

{t('app.loading')}

+
+
+ ) : ( +
+ {/* 服务器总数 */} +
+
+
+ + + +
+
+

{t('pages.dashboard.totalServers')}

+

{serverStats.total}

+
+
+
+ + {/* 在线服务器 */} +
+
+
+ + + +
+
+

{t('pages.dashboard.onlineServers')}

+

{serverStats.online}

+
+
+
+
+
+
+ + {/* 离线服务器 */} +
+
+
+ + + +
+
+

{t('pages.dashboard.offlineServers')}

+

{serverStats.offline}

+
+
+
+
+
+
+ + {/* 连接中服务器 */} +
+
+
+ + + +
+
+

{t('pages.dashboard.connectingServers')}

+

{serverStats.connecting}

+
+
+
+
+
+
+
+ )} + + {/* 最近活动列表 */} + {servers.length > 0 && !isLoading && ( +
+

{t('pages.dashboard.recentServers')}

+
+ + + + + + + + + + + {servers.slice(0, 5).map((server, index) => ( + + + + + + + ))} + +
+ {t('server.name')} + + {t('server.status')} + + {t('server.tools')} + + {t('server.enabled')} +
+ {server.name} + + + {t(statusTranslations[server.status] || server.status)} + + + {server.tools?.length || 0} + + {server.enabled !== false ? ( + + ) : ( + + )} +
+
+
+ )} +
+ ); +}; + +export default DashboardPage; \ No newline at end of file diff --git a/frontend/src/pages/ServersPage.tsx b/frontend/src/pages/ServersPage.tsx new file mode 100644 index 0000000..e3c12a8 --- /dev/null +++ b/frontend/src/pages/ServersPage.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Server } from '@/types'; +import ServerCard from '@/components/ServerCard'; +import AddServerForm from '@/components/AddServerForm'; +import EditServerForm from '@/components/EditServerForm'; +import { useServerData } from '@/hooks/useServerData'; + +const ServersPage: React.FC = () => { + const { t } = useTranslation(); + const { + servers, + error, + setError, + isLoading, + handleServerAdd, + handleServerEdit, + handleServerRemove, + handleServerToggle + } = useServerData(); + const [editingServer, setEditingServer] = useState(null); + + const handleEditClick = async (server: Server) => { + const fullServerData = await handleServerEdit(server); + if (fullServerData) { + setEditingServer(fullServerData); + } + }; + + const handleEditComplete = () => { + setEditingServer(null); + }; + + return ( +
+
+

{t('pages.servers.title')}

+
+ + +
+
+ + {error && ( +
+
+
+

{t('app.error')}

+

{error}

+
+ +
+
+ )} + + {isLoading ? ( +
+
+ + + + +

{t('app.loading')}

+
+
+ ) : servers.length === 0 ? ( +
+

{t('app.noServers')}

+
+ ) : ( +
+ {servers.map((server, index) => ( + + ))} +
+ )} + + {editingServer && ( + setEditingServer(null)} + /> + )} +
+ ); +}; + +export default ServersPage; \ No newline at end of file diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..c653569 --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import ChangePasswordForm from '@/components/ChangePasswordForm'; + +const SettingsPage: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const handlePasswordChangeSuccess = () => { + setTimeout(() => { + navigate('/'); + }, 2000); + }; + + return ( +
+

{t('pages.settings.title')}

+ +
+

{t('auth.changePassword')}

+
+ +
+
+ + {/* 其他设置可以在这里添加 */} +
+

{t('pages.settings.language')}

+
+ + +
+
+
+ ); +}; + +export default SettingsPage; \ No newline at end of file