Enhance routing and settings functionality (#29)

This commit is contained in:
samanhappy
2025-04-21 17:51:21 +08:00
committed by GitHub
parent 6bf22025e1
commit afd1ee7a50
21 changed files with 541 additions and 131 deletions

View File

@@ -7,7 +7,7 @@ const DashboardPage: React.FC = () => {
const { t } = useTranslation();
const { servers, error, setError, isLoading } = useServerData();
// 计算服务器统计信息
// Calculate server statistics
const serverStats = {
total: servers.length,
online: servers.filter(server => server.status === 'connected').length,
@@ -22,7 +22,7 @@ const DashboardPage: React.FC = () => {
connecting: 'status.connecting'
}
// 计算各状态百分比(用于仪表板展示)
// Calculate percentage for each status (for dashboard display)
const getStatusPercentage = (status: ServerStatus) => {
if (servers.length === 0) return 0;
return Math.round((servers.filter(server => server.status === status).length / servers.length) * 100);
@@ -64,7 +64,7 @@ const DashboardPage: React.FC = () => {
</div>
) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{/* 服务器总数 */}
{/* Total servers */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-3 rounded-full bg-blue-100 text-blue-800">
@@ -79,7 +79,7 @@ const DashboardPage: React.FC = () => {
</div>
</div>
{/* 在线服务器 */}
{/* Online servers */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-3 rounded-full bg-green-100 text-green-800">
@@ -100,7 +100,7 @@ const DashboardPage: React.FC = () => {
</div>
</div>
{/* 离线服务器 */}
{/* Offline servers */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-3 rounded-full bg-red-100 text-red-800">
@@ -121,7 +121,7 @@ const DashboardPage: React.FC = () => {
</div>
</div>
{/* 连接中服务器 */}
{/* Connecting servers */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-3 rounded-full bg-yellow-100 text-yellow-800">
@@ -144,7 +144,7 @@ const DashboardPage: React.FC = () => {
</div>
)}
{/* 最近活动列表 */}
{/* Recent activity list */}
{servers.length > 0 && !isLoading && (
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('pages.dashboard.recentServers')}</h2>

View File

@@ -193,7 +193,7 @@ const MarketPage: React.FC = () => {
<div className="md:w-48 flex-shrink-0">
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4">
{/* Categories */}
{categories.length > 0 && (
{categories.length > 0 ? (
<div className="mb-6">
<div className="flex justify-between items-center mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
@@ -218,6 +218,26 @@ const MarketPage: React.FC = () => {
))}
</div>
</div>
) : loading ? (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<div className="flex flex-col gap-2 items-center py-4">
<svg className="animate-spin h-6 w-6 text-blue-500 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-sm text-gray-600">{t('app.loading')}</p>
</div>
</div>
) : (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
</div>
)}
{/* Tags */}

View File

@@ -16,9 +16,11 @@ const ServersPage: React.FC = () => {
handleServerAdd,
handleServerEdit,
handleServerRemove,
handleServerToggle
handleServerToggle,
triggerRefresh
} = useServerData();
const [editingServer, setEditingServer] = useState<Server | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const handleEditClick = async (server: Server) => {
const fullServerData = await handleServerEdit(server);
@@ -31,6 +33,17 @@ const ServersPage: React.FC = () => {
setEditingServer(null);
};
const handleRefresh = async () => {
setIsRefreshing(true);
try {
triggerRefresh();
// Add a slight delay to make the spinner visible
await new Promise(resolve => setTimeout(resolve, 500));
} finally {
setIsRefreshing(false);
}
};
return (
<div>
<div className="flex justify-between items-center mb-8">
@@ -38,12 +51,20 @@ const ServersPage: React.FC = () => {
<div className="flex space-x-4">
<AddServerForm onAdd={handleServerAdd} />
<button
onClick={() => handleServerAdd()}
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center"
onClick={handleRefresh}
disabled={isRefreshing}
className={`px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center ${isRefreshing ? 'opacity-70 cursor-not-allowed' : ''}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
</svg>
{isRefreshing ? (
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
</svg>
)}
{t('common.refresh')}
</button>
</div>

View File

@@ -1,52 +1,146 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import ChangePasswordForm from '@/components/ChangePasswordForm';
import { Switch } from '@/components/ui/ToggleGroup';
import { useSettingsData } from '@/hooks/useSettingsData';
import { useToast } from '@/contexts/ToastContext';
const SettingsPage: React.FC = () => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { showToast } = useToast();
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
// Update current language when it changes
useEffect(() => {
setCurrentLanguage(i18n.language);
}, [i18n.language]);
const {
routingConfig,
loading,
updateRoutingConfig
} = useSettingsData();
const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false,
password: false
});
const toggleSection = (section: 'routingConfig' | 'password') => {
setSectionsVisible(prev => ({
...prev,
[section]: !prev[section]
}));
};
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute', value: boolean) => {
await updateRoutingConfig(key, value);
};
const handlePasswordChangeSuccess = () => {
setTimeout(() => {
navigate('/');
}, 2000);
};
const handleLanguageChange = (lang: string) => {
localStorage.setItem('i18nextLng', lang);
window.location.reload();
};
return (
<div>
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-8">{t('pages.settings.title')}</h1>
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('auth.changePassword')}</h2>
<div className="max-w-lg">
<ChangePasswordForm onSuccess={handlePasswordChangeSuccess} />
{/* Language Settings */}
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.language')}</h2>
<div className="flex space-x-3">
<button
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
currentLanguage.startsWith('en')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
onClick={() => handleLanguageChange('en')}
>
English
</button>
<button
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
currentLanguage.startsWith('zh')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
onClick={() => handleLanguageChange('zh')}
>
</button>
</div>
</div>
</div>
{/* 其他设置可以在这里添加 */}
<div className="bg-white shadow rounded-lg p-6 mt-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('pages.settings.language')}</h2>
<div className="flex space-x-4">
<button
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200"
onClick={() => {
localStorage.setItem('i18nextLng', 'en');
window.location.reload();
}}
>
English
</button>
<button
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200"
onClick={() => {
localStorage.setItem('i18nextLng', 'zh');
window.location.reload();
}}
>
</button>
{/* Route Configuration Settings */}
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('routingConfig')}
>
<h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.routeConfig')}</h2>
<span className="text-gray-500">
{sectionsVisible.routingConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.routingConfig && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableGlobalRouteDescription')}</p>
</div>
<Switch
disabled={loading}
checked={routingConfig.enableGlobalRoute}
onCheckedChange={(checked) => handleRoutingConfigChange('enableGlobalRoute', checked)}
/>
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableGroupNameRouteDescription')}</p>
</div>
<Switch
disabled={loading}
checked={routingConfig.enableGroupNameRoute}
onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
/>
</div>
</div>
)}
</div>
{/* Change Password */}
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('password')}
>
<h2 className="text-xl font-semibold text-gray-800">{t('auth.changePassword')}</h2>
<span className="text-gray-500">
{sectionsVisible.password ? '▼' : '►'}
</span>
</div>
{sectionsVisible.password && (
<div className="max-w-lg mt-4">
<ChangePasswordForm onSuccess={handlePasswordChangeSuccess} />
</div>
)}
</div>
</div>
);