From 94962158ef4ea78e7dd90ec4be0ba4dbe4df72ed Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Mon, 22 Dec 2025 22:41:50 +0800 Subject: [PATCH] feat(settings): move request logging toggle behind hidden entry --- src/components/layout/MainLayout.tsx | 118 ++++++++++++++++++++++++++- src/components/ui/ToggleSwitch.tsx | 15 +++- src/i18n/locales/en.json | 2 + src/i18n/locales/zh-CN.json | 2 + src/pages/DashboardPage.tsx | 6 -- src/pages/SettingsPage.tsx | 20 +---- src/styles/components.scss | 17 ++++ 7 files changed, 151 insertions(+), 29 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index a8474e3e..0481d970 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -2,6 +2,8 @@ import { ReactNode, SVGProps, useCallback, useEffect, useLayoutEffect, useRef, u import { NavLink, Outlet } 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 { IconBot, IconChartLine, @@ -16,7 +18,7 @@ import { } from '@/components/ui/icons'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores'; -import { versionApi } from '@/services/api'; +import { configApi, versionApi } from '@/services/api'; const sidebarIcons: Record = { dashboard: , @@ -148,6 +150,7 @@ export function MainLayout() { 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 toggleTheme = useThemeStore((state) => state.toggleTheme); @@ -157,11 +160,20 @@ export function MainLayout() { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [checkingVersion, setCheckingVersion] = 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 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); // 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动 useLayoutEffect(() => { @@ -203,6 +215,20 @@ export function MainLayout() { }; }, []); + useEffect(() => { + if (requestLogModalOpen && !requestLogTouched) { + setRequestLogDraft(requestLogEnabled); + } + }, [requestLogModalOpen, requestLogTouched, requestLogEnabled]); + + useEffect(() => { + return () => { + if (versionTapTimer.current) { + clearTimeout(versionTapTimer.current); + } + }; + }, []); + const handleBrandClick = useCallback(() => { if (!brandExpanded) { setBrandExpanded(true); @@ -216,6 +242,60 @@ export function MainLayout() { } }, [brandExpanded]); + const openRequestLogModal = useCallback(() => { + setRequestLogTouched(false); + setRequestLogDraft(requestLogEnabled); + setRequestLogModalOpen(true); + }, [requestLogEnabled]); + + 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会提示 @@ -369,7 +449,7 @@ export function MainLayout() { {t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')} - + {t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')} @@ -379,6 +459,40 @@ export function MainLayout() { + + + + + + } + > +
+
{t('basic_settings.request_log_warning')}
+ { + setRequestLogDraft(value); + setRequestLogTouched(true); + }} + /> +
+
); } diff --git a/src/components/ui/ToggleSwitch.tsx b/src/components/ui/ToggleSwitch.tsx index f0b53325..44170419 100644 --- a/src/components/ui/ToggleSwitch.tsx +++ b/src/components/ui/ToggleSwitch.tsx @@ -5,15 +5,26 @@ interface ToggleSwitchProps { onChange: (value: boolean) => void; label?: ReactNode; disabled?: boolean; + labelPosition?: 'left' | 'right'; } -export function ToggleSwitch({ checked, onChange, label, disabled = false }: ToggleSwitchProps) { +export function ToggleSwitch({ + checked, + onChange, + label, + disabled = false, + labelPosition = 'right' +}: ToggleSwitchProps) { const handleChange = (event: ChangeEvent) => { onChange(event.target.checked); }; + const className = ['switch', labelPosition === 'left' ? 'switch-label-left' : ''] + .filter(Boolean) + .join(' '); + return ( -