From aa3448731304b79fbfbdf69f38150f0bad21a3c7 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Tue, 17 Mar 2026 01:09:35 +0800 Subject: [PATCH] refactor(system-page): clean up version check logic and enhance layout --- src/components/layout/MainLayout.tsx | 77 ---------- src/pages/SystemPage.module.scss | 22 ++- src/pages/SystemPage.tsx | 213 +++++++++++++++++++-------- 3 files changed, 173 insertions(+), 139 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 70433647..53307aa2 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -31,7 +31,6 @@ import { useNotificationStore, useThemeStore, } from '@/stores'; -import { versionApi } from '@/services/api'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; import { isSupportedLanguage } from '@/utils/language'; @@ -70,12 +69,6 @@ const headerIcons = { ), - update: ( - - - - - ), menu: ( @@ -209,39 +202,12 @@ const THEME_CARDS: Array<{ }, ]; -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 } = useTranslation(); const { showNotification } = useNotificationStore(); const location = useLocation(); const apiBase = useAuthStore((state) => state.apiBase); - const serverVersion = useAuthStore((state) => state.serverVersion); const connectionStatus = useAuthStore((state) => state.connectionStatus); const logout = useAuthStore((state) => state.logout); @@ -256,7 +222,6 @@ export function MainLayout() { const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [checkingVersion, setCheckingVersion] = useState(false); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); const [themeMenuOpen, setThemeMenuOpen] = useState(false); const [brandExpanded, setBrandExpanded] = useState(true); @@ -542,39 +507,6 @@ export function MainLayout() { showNotification(t('notification.data_refreshed'), 'success'); }; - const handleVersionCheck = async () => { - setCheckingVersion(true); - try { - const data = await versionApi.checkLatest(); - const latestRaw = data?.['latest-version'] ?? data?.latest_version ?? data?.latest ?? ''; - const latest = typeof latestRaw === 'string' ? latestRaw : String(latestRaw ?? ''); - 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: unknown) { - const message = - error instanceof Error ? error.message : typeof error === 'string' ? error : ''; - const suffix = message ? `: ${message}` : ''; - showNotification(`${t('system_info.version_check_error')}${suffix}`, 'error'); - } finally { - setCheckingVersion(false); - } - }; - return (
@@ -632,15 +564,6 @@ export function MainLayout() { > {headerIcons.refresh} -
{ + 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 SystemPage() { const { t, i18n } = useTranslation(); const { showNotification, showConfirmation } = useNotificationStore(); @@ -51,11 +83,15 @@ export function SystemPage() { const modelsError = useModelsStore((state) => state.error); const fetchModelsFromStore = useModelsStore((state) => state.fetchModels); - const [modelStatus, setModelStatus] = useState<{ type: 'success' | 'warning' | 'error' | 'muted'; message: string }>(); + const [modelStatus, setModelStatus] = useState<{ + type: 'success' | 'warning' | 'error' | 'muted'; + message: string; + }>(); const [requestLogModalOpen, setRequestLogModalOpen] = useState(false); const [requestLogDraft, setRequestLogDraft] = useState(false); const [requestLogTouched, setRequestLogTouched] = useState(false); const [requestLogSaving, setRequestLogSaving] = useState(false); + const [checkingVersion, setCheckingVersion] = useState(false); const apiKeysCache = useRef([]); const versionTapCount = useRef(0); @@ -136,7 +172,7 @@ export function SystemPage() { if (auth.connectionStatus !== 'connected') { setModelStatus({ type: 'warning', - message: t('notification.connection_required') + message: t('notification.connection_required'), }); return; } @@ -158,11 +194,12 @@ export function SystemPage() { const hasModels = list.length > 0; setModelStatus({ type: hasModels ? 'success' : 'warning', - message: hasModels ? t('system_info.models_count', { count: list.length }) : t('system_info.models_empty') + message: hasModels + ? t('system_info.models_count', { count: list.length }) + : t('system_info.models_empty'), }); } catch (err: unknown) { - const message = - err instanceof Error ? err.message : typeof err === 'string' ? err : ''; + const message = err instanceof Error ? err.message : typeof err === 'string' ? err : ''; const suffix = message ? `: ${message}` : ''; const text = `${t('system_info.models_error')}${suffix}`; setModelStatus({ type: 'error', message: text }); @@ -244,6 +281,39 @@ export function SystemPage() { } }; + const handleVersionCheck = useCallback(async () => { + setCheckingVersion(true); + try { + const data = await versionApi.checkLatest(); + const latestRaw = data?.['latest-version'] ?? data?.latest_version ?? data?.latest ?? ''; + const latest = typeof latestRaw === 'string' ? latestRaw : String(latestRaw ?? ''); + const comparison = compareVersions(latest, auth.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: unknown) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : ''; + const suffix = message ? `: ${message}` : ''; + showNotification(`${t('system_info.version_check_error')}${suffix}`, 'error'); + } finally { + setCheckingVersion(false); + } + }, [auth.serverVersion, showNotification, t]); + useEffect(() => { fetchConfig().catch(() => { // ignore @@ -290,7 +360,21 @@ export function SystemPage() {
-
{t('footer.api_version')}
+
+
{t('footer.api_version')}
+ +
{apiVersion}
@@ -366,61 +450,70 @@ export function SystemPage() {
- fetchModels({ forceRefresh: true })} loading={modelsLoading}> - {t('common.refresh')} - - } - > -

{t('system_info.models_desc')}

- {modelStatus &&
{modelStatus.message}
} - {modelsError &&
{modelsError}
} - {modelsLoading ? ( -
{t('common.loading')}
- ) : models.length === 0 ? ( -
{t('system_info.models_empty')}
- ) : ( -
- {groupedModels.map((group) => { - const iconSrc = getIconForCategory(group.id); - return ( -
-
-
- {iconSrc && } - {group.label} + fetchModels({ forceRefresh: true })} + loading={modelsLoading} + > + {t('common.refresh')} + + } + > +

{t('system_info.models_desc')}

+ {modelStatus && ( +
{modelStatus.message}
+ )} + {modelsError &&
{modelsError}
} + {modelsLoading ? ( +
{t('common.loading')}
+ ) : models.length === 0 ? ( +
{t('system_info.models_empty')}
+ ) : ( +
+ {groupedModels.map((group) => { + const iconSrc = getIconForCategory(group.id); + return ( +
+
+
+ {iconSrc && } + {group.label} +
+
+ {t('system_info.models_count', { count: group.items.length })} +
+
+
+ {group.items.map((model) => ( + + {model.name} + {model.alias && {model.alias}} + + ))}
-
{t('system_info.models_count', { count: group.items.length })}
-
- {group.items.map((model) => ( - - {model.name} - {model.alias && {model.alias}} - - ))} -
-
- ); - })} -
- )} - + ); + })} +
+ )} + - -

{t('system_info.clear_login_desc')}

-
- -
-
+ +

{t('system_info.clear_login_desc')}

+
+ +
+