From e053854544ee96d2c405e2daec0b1fc7594149d9 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 7 Feb 2026 12:03:40 +0800 Subject: [PATCH] feat(system): redesign system info page and move request-log controls from layout footer --- src/components/layout/MainLayout.tsx | 137 +------------------- src/i18n/locales/en.json | 1 + src/i18n/locales/ru.json | 1 + src/i18n/locales/zh-CN.json | 1 + src/pages/SystemPage.module.scss | 128 +++++++++++++++++++ src/pages/SystemPage.tsx | 181 +++++++++++++++++++++++---- src/styles/layout.scss | 21 ---- 7 files changed, 292 insertions(+), 178 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 6e8aaa0..074d9ea 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -10,8 +10,6 @@ import { import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; -import { Modal } from '@/components/ui/Modal'; -import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { PageTransition } from '@/components/common/PageTransition'; import { MainRoutes } from '@/router/MainRoutes'; import { @@ -33,7 +31,7 @@ import { useNotificationStore, useThemeStore, } from '@/stores'; -import { configApi, versionApi } from '@/services/api'; +import { versionApi } from '@/services/api'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; import { isSupportedLanguage } from '@/utils/language'; @@ -174,20 +172,18 @@ const compareVersions = (latest?: string | null, current?: string | null) => { }; export function MainLayout() { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const location = useLocation(); 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 logout = useAuthStore((state) => state.logout); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const clearCache = useConfigStore((state) => state.clearCache); - const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const theme = useThemeStore((state) => state.theme); const cycleTheme = useThemeStore((state) => state.cycleTheme); @@ -199,22 +195,13 @@ export function MainLayout() { const [checkingVersion, setCheckingVersion] = useState(false); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); const [brandExpanded, setBrandExpanded] = useState(true); - const [requestLogModalOpen, setRequestLogModalOpen] = useState(false); - const [requestLogDraft, setRequestLogDraft] = useState(false); - const [requestLogTouched, setRequestLogTouched] = useState(false); - const [requestLogSaving, setRequestLogSaving] = useState(false); const contentRef = useRef(null); const languageMenuRef = useRef(null); const brandCollapseTimer = useRef | null>(null); const headerRef = useRef(null); - const versionTapCount = useRef(0); - const versionTapTimer = useRef | null>(null); const fullBrandName = 'CLI Proxy API Management Center'; const abbrBrandName = t('title.abbr'); - const requestLogEnabled = config?.requestLog ?? false; - const requestLogDirty = requestLogDraft !== requestLogEnabled; - const canEditRequestLog = connectionStatus === 'connected' && Boolean(config); const isLogsPage = location.pathname.startsWith('/logs'); // 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动 @@ -246,7 +233,7 @@ export function MainLayout() { }; }, []); - // 将主内容区的中心点写入 CSS 变量,供底部浮层(如配置面板操作栏)对齐到内容区而非整窗 + // 将主内容区的中心点写入 CSS 变量,供底部浮层(配置面板操作栏、提供商导航)对齐到内容区 useLayoutEffect(() => { const updateContentCenter = () => { const el = contentRef.current; @@ -274,6 +261,7 @@ export function MainLayout() { resizeObserver.disconnect(); } window.removeEventListener('resize', updateContentCenter); + document.documentElement.style.removeProperty('--content-center-x'); }; }, []); @@ -290,20 +278,6 @@ export function MainLayout() { }; }, []); - useEffect(() => { - if (requestLogModalOpen && !requestLogTouched) { - setRequestLogDraft(requestLogEnabled); - } - }, [requestLogModalOpen, requestLogTouched, requestLogEnabled]); - - useEffect(() => { - return () => { - if (versionTapTimer.current) { - clearTimeout(versionTapTimer.current); - } - }; - }, []); - useEffect(() => { if (!languageMenuOpen) { return; @@ -343,12 +317,6 @@ export function MainLayout() { } }, [brandExpanded]); - const openRequestLogModal = useCallback(() => { - setRequestLogTouched(false); - setRequestLogDraft(requestLogEnabled); - setRequestLogModalOpen(true); - }, [requestLogEnabled]); - const toggleLanguageMenu = useCallback(() => { setLanguageMenuOpen((prev) => !prev); }, []); @@ -364,54 +332,6 @@ export function MainLayout() { [setLanguage] ); - const handleRequestLogClose = useCallback(() => { - setRequestLogModalOpen(false); - setRequestLogTouched(false); - }, []); - - const handleVersionTap = useCallback(() => { - versionTapCount.current += 1; - if (versionTapTimer.current) { - clearTimeout(versionTapTimer.current); - } - versionTapTimer.current = setTimeout(() => { - versionTapCount.current = 0; - }, 1500); - - if (versionTapCount.current >= 7) { - versionTapCount.current = 0; - if (versionTapTimer.current) { - clearTimeout(versionTapTimer.current); - versionTapTimer.current = null; - } - openRequestLogModal(); - } - }, [openRequestLogModal]); - - const handleRequestLogSave = async () => { - if (!canEditRequestLog) return; - if (!requestLogDirty) { - setRequestLogModalOpen(false); - return; - } - - const previous = requestLogEnabled; - setRequestLogSaving(true); - updateConfigValue('request-log', requestLogDraft); - - try { - await configApi.updateRequestLog(requestLogDraft); - clearCache('request-log'); - showNotification(t('notification.request_log_updated'), 'success'); - setRequestLogModalOpen(false); - } catch (error: any) { - updateConfigValue('request-log', previous); - showNotification(`${t('notification.update_failed')}: ${error?.message || ''}`, 'error'); - } finally { - setRequestLogSaving(false); - } - }; - useEffect(() => { fetchConfig().catch(() => { // ignore initial failure; login flow会提示 @@ -685,57 +605,8 @@ export function MainLayout() { scrollContainerRef={contentRef} /> - -
- - {t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')} - - - {t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')} - - - {t('footer.build_date')}:{' '} - {serverBuildDate - ? new Date(serverBuildDate).toLocaleString(i18n.language) - : t('system_info.version_unknown')} - -
- - - - - - } - > -
-
{t('basic_settings.request_log_warning')}
- { - setRequestLogDraft(value); - setRequestLogTouched(true); - }} - /> -
-
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b07a09a..e6a2276 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -989,6 +989,7 @@ }, "system_info": { "title": "Management Center Info", + "about_title": "CLI Proxy API Management Center", "connection_status_title": "Connection Status", "api_status_label": "API Status:", "config_status_label": "Config Status:", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 2bc9eaa..d30a9eb 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -994,6 +994,7 @@ }, "system_info": { "title": "Информация о центре управления", + "about_title": "CLI Proxy API Management Center", "connection_status_title": "Статус подключения", "api_status_label": "Статус API:", "config_status_label": "Статус конфигурации:", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 6ea4000..8e00018 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -989,6 +989,7 @@ }, "system_info": { "title": "管理中心信息", + "about_title": "CLI Proxy API Management Center", "connection_status_title": "连接状态", "api_status_label": "API 状态:", "config_status_label": "配置状态:", diff --git a/src/pages/SystemPage.module.scss b/src/pages/SystemPage.module.scss index 459b741..dbef6f1 100644 --- a/src/pages/SystemPage.module.scss +++ b/src/pages/SystemPage.module.scss @@ -15,6 +15,108 @@ gap: $spacing-xl; } +.aboutCard { + overflow: hidden; +} + +.aboutHeader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: $spacing-md; + padding: $spacing-lg 0 $spacing-xl; +} + +.aboutLogo { + width: 108px; + height: 108px; + border-radius: 26px; + object-fit: cover; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); +} + +.aboutTitle { + width: min(100%, 920px); + font-size: clamp(28px, 4.2vw, 44px); + font-weight: 800; + line-height: 1.12; + color: var(--text-primary); + letter-spacing: -0.02em; + text-align: center; + text-wrap: balance; + white-space: normal; + overflow-wrap: anywhere; +} + +.aboutInfoGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: $spacing-md; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + } +} + +.infoTile { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 120px; + padding: $spacing-md $spacing-lg; + border-radius: $radius-lg; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-secondary) 82%, transparent); + text-align: left; +} + +.tapTile { + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-secondary) 82%, transparent); + color: inherit; + padding: $spacing-md $spacing-lg; + cursor: pointer; + transition: transform 0.18s ease, border-color 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-1px); + border-color: var(--primary-color); + box-shadow: 0 8px 18px rgba(59, 130, 246, 0.15); + } + + &:active { + transform: translateY(0); + } +} + +.tileLabel { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); +} + +.tileValue { + font-size: 22px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.25; + word-break: break-word; +} + +.tileSub { + font-size: 12px; + color: var(--text-tertiary); + line-height: 1.4; +} + +.aboutActions { + display: flex; + justify-content: flex-end; + margin-top: $spacing-lg; +} + .section { display: flex; flex-direction: column; @@ -231,3 +333,29 @@ overflow: hidden; text-overflow: ellipsis; } + +@media (max-width: 768px) { + .aboutLogo { + width: 92px; + height: 92px; + border-radius: 22px; + } + + .aboutTitle { + width: min(100%, 24ch); + font-size: clamp(22px, 6.6vw, 34px); + font-weight: 700; + line-height: 1.18; + letter-spacing: -0.012em; + } +} + +@media (max-width: 520px) { + .aboutTitle { + width: min(100%, 19ch); + font-size: clamp(20px, 7.2vw, 28px); + font-weight: 600; + line-height: 1.22; + letter-spacing: -0.006em; + } +} diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index d428aeb..e4f3069 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -2,11 +2,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; +import { Modal } from '@/components/ui/Modal'; +import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/components/ui/icons'; import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore, useThemeStore } from '@/stores'; +import { configApi } from '@/services/api'; import { apiKeysApi } from '@/services/api/apiKeys'; import { classifyModels } from '@/utils/models'; import { STORAGE_KEY_AUTH } from '@/utils/constants'; +import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import iconGemini from '@/assets/icons/gemini.svg'; import iconClaude from '@/assets/icons/claude.svg'; import iconOpenaiLight from '@/assets/icons/openai-light.svg'; @@ -39,6 +43,8 @@ export function SystemPage() { const auth = useAuthStore(); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); + const clearCache = useConfigStore((state) => state.clearCache); + const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const models = useModelsStore((state) => state.models); const modelsLoading = useModelsStore((state) => state.loading); @@ -46,14 +52,29 @@ export function SystemPage() { const fetchModelsFromStore = useModelsStore((state) => state.fetchModels); 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 apiKeysCache = useRef([]); + const versionTapCount = useRef(0); + const versionTapTimer = useRef | null>(null); const otherLabel = useMemo( () => (i18n.language?.toLowerCase().startsWith('zh') ? '其他' : 'Other'), [i18n.language] ); const groupedModels = useMemo(() => classifyModels(models, { otherLabel }), [models, otherLabel]); + const requestLogEnabled = config?.requestLog ?? false; + const requestLogDirty = requestLogDraft !== requestLogEnabled; + const canEditRequestLog = auth.connectionStatus === 'connected' && Boolean(config); + + const appVersion = __APP_VERSION__ || t('system_info.version_unknown'); + const apiVersion = auth.serverVersion || t('system_info.version_unknown'); + const buildTime = auth.serverBuildDate + ? new Date(auth.serverBuildDate).toLocaleString(i18n.language) + : t('system_info.version_unknown'); const getIconForCategory = (categoryId: string): string | null => { const iconEntry = MODEL_CATEGORY_ICONS[categoryId]; @@ -152,12 +173,80 @@ export function SystemPage() { }); }; + const openRequestLogModal = useCallback(() => { + setRequestLogTouched(false); + setRequestLogDraft(requestLogEnabled); + setRequestLogModalOpen(true); + }, [requestLogEnabled]); + + const handleInfoVersionTap = useCallback(() => { + versionTapCount.current += 1; + if (versionTapTimer.current) { + clearTimeout(versionTapTimer.current); + } + + if (versionTapCount.current >= 7) { + versionTapCount.current = 0; + versionTapTimer.current = null; + openRequestLogModal(); + return; + } + + versionTapTimer.current = setTimeout(() => { + versionTapCount.current = 0; + versionTapTimer.current = null; + }, 1500); + }, [openRequestLogModal]); + + const handleRequestLogClose = useCallback(() => { + setRequestLogModalOpen(false); + setRequestLogTouched(false); + }, []); + + const handleRequestLogSave = async () => { + if (!canEditRequestLog) return; + if (!requestLogDirty) { + setRequestLogModalOpen(false); + return; + } + + const previous = requestLogEnabled; + setRequestLogSaving(true); + updateConfigValue('request-log', requestLogDraft); + + try { + await configApi.updateRequestLog(requestLogDraft); + clearCache('request-log'); + showNotification(t('notification.request_log_updated'), 'success'); + setRequestLogModalOpen(false); + } catch (error: any) { + updateConfigValue('request-log', previous); + showNotification(`${t('notification.update_failed')}: ${error?.message || ''}`, 'error'); + } finally { + setRequestLogSaving(false); + } + }; + useEffect(() => { fetchConfig().catch(() => { // ignore }); }, [fetchConfig]); + useEffect(() => { + if (requestLogModalOpen && !requestLogTouched) { + setRequestLogDraft(requestLogEnabled); + } + }, [requestLogModalOpen, requestLogTouched, requestLogEnabled]); + + useEffect(() => { + return () => { + if (versionTapTimer.current) { + clearTimeout(versionTapTimer.current); + } + }; + }, []); + useEffect(() => { fetchModels(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -167,33 +256,43 @@ export function SystemPage() {

{t('system_info.title')}

- +
+ CPAMC +
{t('system_info.about_title')}
+
+ +
+ + +
+
{t('footer.api_version')}
+
{apiVersion}
+
+ +
+
{t('footer.build_date')}
+
{buildTime}
+
+ +
+
{t('connection.status')}
+
{t(`common.${auth.connectionStatus}_status` as any)}
+
{auth.apiBase || '-'}
+
+
+ +
- } - > -
-
-
{t('connection.server_address')}
-
{auth.apiBase || '-'}
-
-
-
{t('footer.api_version')}
-
{auth.serverVersion || t('system_info.version_unknown')}
-
-
-
{t('footer.build_date')}
-
- {auth.serverBuildDate ? new Date(auth.serverBuildDate).toLocaleString() : t('system_info.version_unknown')} -
-
-
-
{t('connection.status')}
-
{t(`common.${auth.connectionStatus}_status` as any)}
-
@@ -312,6 +411,40 @@ export function SystemPage() {
+ + + + + + } + > +
+
{t('basic_settings.request_log_warning')}
+ { + setRequestLogDraft(value); + setRequestLogTouched(true); + }} + /> +
+
); } diff --git a/src/styles/layout.scss b/src/styles/layout.scss index b6d041e..83f102b 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -448,27 +448,6 @@ } } -.footer { - padding: $spacing-md $spacing-lg; - border-top: 1px solid var(--border-color); - background: var(--bg-primary); - display: flex; - justify-content: space-between; - align-items: center; - color: var(--text-secondary); - font-size: 14px; - flex-wrap: wrap; - gap: $spacing-sm; - - .footer-version { - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - -webkit-touch-callout: none; - } -} - - .grid { display: grid; gap: $spacing-lg;