import { ReactNode, SVGProps, useEffect, useMemo, useState } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores'; import { versionApi } from '@/services/api'; import { isLocalhost } from '@/utils/connection'; const iconProps: SVGProps = { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false' }; const sidebarIcons: Record = { settings: ( ), apiKeys: ( ), aiProviders: ( ), authFiles: ( ), oauth: ( ), usage: ( ), config: ( ), logs: ( ), system: ( ) }; const parseVersionSegments = (version?: string | null) => { if (!version) return null; const cleaned = version.trim().replace(/^v/i, ''); if (!cleaned) return null; const parts = cleaned .split(/[^0-9]+/) .filter(Boolean) .map((segment) => Number.parseInt(segment, 10)) .filter(Number.isFinite); return parts.length ? parts : null; }; const compareVersions = (latest?: string | null, current?: string | null) => { const latestParts = parseVersionSegments(latest); const currentParts = parseVersionSegments(current); if (!latestParts || !currentParts) return null; const length = Math.max(latestParts.length, currentParts.length); for (let i = 0; i < length; i++) { const l = latestParts[i] || 0; const c = currentParts[i] || 0; if (l > c) return 1; if (l < c) return -1; } return 0; }; export function MainLayout() { const { t, i18n } = useTranslation(); const { showNotification } = useNotificationStore(); const apiBase = useAuthStore((state) => state.apiBase); const serverVersion = useAuthStore((state) => state.serverVersion); const serverBuildDate = useAuthStore((state) => state.serverBuildDate); const connectionStatus = useAuthStore((state) => state.connectionStatus); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const clearCache = useConfigStore((state) => state.clearCache); const theme = useThemeStore((state) => state.theme); const toggleTheme = useThemeStore((state) => state.toggleTheme); const language = useLanguageStore((state) => state.language); const toggleLanguage = useLanguageStore((state) => state.toggleLanguage); const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [checkingVersion, setCheckingVersion] = useState(false); const isLocal = useMemo(() => isLocalhost(window.location.hostname), []); useEffect(() => { fetchConfig().catch(() => { // ignore initial failure; login flow会提示 }); }, [fetchConfig]); const statusClass = connectionStatus === 'connected' ? 'success' : connectionStatus === 'connecting' ? 'warning' : connectionStatus === 'error' ? 'error' : 'muted'; const navItems = [ { path: '/settings', label: t('nav.basic_settings'), icon: sidebarIcons.settings }, { path: '/api-keys', label: t('nav.api_keys'), icon: sidebarIcons.apiKeys }, { path: '/ai-providers', label: t('nav.ai_providers'), icon: sidebarIcons.aiProviders }, { path: '/auth-files', label: t('nav.auth_files'), icon: sidebarIcons.authFiles }, ...(isLocal ? [{ path: '/oauth', label: t('nav.oauth', { defaultValue: 'OAuth' }), icon: sidebarIcons.oauth }] : []), { path: '/usage', label: t('nav.usage_stats'), icon: sidebarIcons.usage }, { path: '/config', label: t('nav.config_management'), icon: sidebarIcons.config }, ...(config?.loggingToFile ? [{ path: '/logs', label: t('nav.logs'), icon: sidebarIcons.logs }] : []), { path: '/system', label: t('nav.system_info'), icon: sidebarIcons.system } ]; const handleRefreshAll = async () => { clearCache(); try { await fetchConfig(undefined, true); showNotification(t('notification.data_refreshed'), 'success'); } catch (error: any) { showNotification(`${t('notification.refresh_failed')}: ${error?.message || ''}`, 'error'); } }; const handleVersionCheck = async () => { setCheckingVersion(true); try { const data = await versionApi.checkLatest(); const latest = data?.['latest-version'] ?? data?.latest_version ?? data?.latest ?? ''; const comparison = compareVersions(latest, serverVersion); if (!latest) { showNotification(t('system_info.version_check_error'), 'error'); return; } if (comparison === null) { showNotification(t('system_info.version_current_missing'), 'warning'); return; } if (comparison > 0) { showNotification(t('system_info.version_update_available', { version: latest }), 'warning'); } else { showNotification(t('system_info.version_is_latest'), 'success'); } } catch (error: any) { showNotification(`${t('system_info.version_check_error')}: ${error?.message || ''}`, 'error'); } finally { setCheckingVersion(false); } }; return (
{t( connectionStatus === 'connected' ? 'common.connected_status' : connectionStatus === 'connecting' ? 'common.connecting_status' : 'common.disconnected_status' )} {apiBase || '-'}
); }