feat(settings): move request logging toggle behind hidden entry

This commit is contained in:
Supra4E8C
2025-12-22 22:41:50 +08:00
parent 68974ffc68
commit 94962158ef
7 changed files with 151 additions and 29 deletions

View File

@@ -2,6 +2,8 @@ import { ReactNode, SVGProps, useCallback, useEffect, useLayoutEffect, useRef, u
import { NavLink, Outlet } from 'react-router-dom'; import { NavLink, Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { import {
IconBot, IconBot,
IconChartLine, IconChartLine,
@@ -16,7 +18,7 @@ import {
} from '@/components/ui/icons'; } from '@/components/ui/icons';
import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores'; import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
import { versionApi } from '@/services/api'; import { configApi, versionApi } from '@/services/api';
const sidebarIcons: Record<string, ReactNode> = { const sidebarIcons: Record<string, ReactNode> = {
dashboard: <IconLayoutDashboard size={18} />, dashboard: <IconLayoutDashboard size={18} />,
@@ -148,6 +150,7 @@ export function MainLayout() {
const config = useConfigStore((state) => state.config); const config = useConfigStore((state) => state.config);
const fetchConfig = useConfigStore((state) => state.fetchConfig); const fetchConfig = useConfigStore((state) => state.fetchConfig);
const clearCache = useConfigStore((state) => state.clearCache); const clearCache = useConfigStore((state) => state.clearCache);
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
const theme = useThemeStore((state) => state.theme); const theme = useThemeStore((state) => state.theme);
const toggleTheme = useThemeStore((state) => state.toggleTheme); const toggleTheme = useThemeStore((state) => state.toggleTheme);
@@ -157,11 +160,20 @@ export function MainLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [checkingVersion, setCheckingVersion] = useState(false); const [checkingVersion, setCheckingVersion] = useState(false);
const [brandExpanded, setBrandExpanded] = useState(true); 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<ReturnType<typeof setTimeout> | null>(null); const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const headerRef = useRef<HTMLElement | null>(null); const headerRef = useRef<HTMLElement | null>(null);
const versionTapCount = useRef(0);
const versionTapTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const fullBrandName = 'CLI Proxy API Management Center'; const fullBrandName = 'CLI Proxy API Management Center';
const abbrBrandName = t('title.abbr'); const abbrBrandName = t('title.abbr');
const requestLogEnabled = config?.requestLog ?? false;
const requestLogDirty = requestLogDraft !== requestLogEnabled;
const canEditRequestLog = connectionStatus === 'connected' && Boolean(config);
// 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动 // 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动
useLayoutEffect(() => { 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(() => { const handleBrandClick = useCallback(() => {
if (!brandExpanded) { if (!brandExpanded) {
setBrandExpanded(true); setBrandExpanded(true);
@@ -216,6 +242,60 @@ export function MainLayout() {
} }
}, [brandExpanded]); }, [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(() => { useEffect(() => {
fetchConfig().catch(() => { fetchConfig().catch(() => {
// ignore initial failure; login flow会提示 // ignore initial failure; login flow会提示
@@ -369,7 +449,7 @@ export function MainLayout() {
<span> <span>
{t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')} {t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')}
</span> </span>
<span> <span onClick={handleVersionTap}>
{t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')} {t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')}
</span> </span>
<span> <span>
@@ -379,6 +459,40 @@ export function MainLayout() {
</footer> </footer>
</div> </div>
</div> </div>
<Modal
open={requestLogModalOpen}
onClose={handleRequestLogClose}
title={t('basic_settings.request_log_title')}
footer={
<>
<Button variant="secondary" onClick={handleRequestLogClose} disabled={requestLogSaving}>
{t('common.cancel')}
</Button>
<Button
onClick={handleRequestLogSave}
loading={requestLogSaving}
disabled={!canEditRequestLog || !requestLogDirty}
>
{t('common.save')}
</Button>
</>
}
>
<div className="request-log-modal">
<div className="status-badge warning">{t('basic_settings.request_log_warning')}</div>
<ToggleSwitch
label={t('basic_settings.request_log_enable')}
labelPosition="left"
checked={requestLogDraft}
disabled={!canEditRequestLog || requestLogSaving}
onChange={(value) => {
setRequestLogDraft(value);
setRequestLogTouched(true);
}}
/>
</div>
</Modal>
</div> </div>
); );
} }

View File

@@ -5,15 +5,26 @@ interface ToggleSwitchProps {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
label?: ReactNode; label?: ReactNode;
disabled?: boolean; 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<HTMLInputElement>) => { const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.checked); onChange(event.target.checked);
}; };
const className = ['switch', labelPosition === 'left' ? 'switch-label-left' : '']
.filter(Boolean)
.join(' ');
return ( return (
<label className="switch"> <label className={className}>
<input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} /> <input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} />
<span className="track"> <span className="track">
<span className="thumb" /> <span className="thumb" />

View File

@@ -133,7 +133,9 @@
"usage_statistics_enable": "Enable usage statistics", "usage_statistics_enable": "Enable usage statistics",
"logging_title": "Logging", "logging_title": "Logging",
"logging_to_file_enable": "Enable logging to file", "logging_to_file_enable": "Enable logging to file",
"request_log_title": "Request Logging",
"request_log_enable": "Enable request logging", "request_log_enable": "Enable request logging",
"request_log_warning": "Keep this off unless you need detailed troubleshooting.",
"ws_auth_title": "WebSocket Authentication", "ws_auth_title": "WebSocket Authentication",
"ws_auth_enable": "Require auth for /ws/*" "ws_auth_enable": "Require auth for /ws/*"
}, },

View File

@@ -133,7 +133,9 @@
"usage_statistics_enable": "启用使用统计", "usage_statistics_enable": "启用使用统计",
"logging_title": "日志记录", "logging_title": "日志记录",
"logging_to_file_enable": "启用日志记录到文件", "logging_to_file_enable": "启用日志记录到文件",
"request_log_title": "请求日志",
"request_log_enable": "启用请求日志", "request_log_enable": "启用请求日志",
"request_log_warning": "仅在需要排查问题时开启,日常请保持关闭。",
"ws_auth_title": "WebSocket 鉴权", "ws_auth_title": "WebSocket 鉴权",
"ws_auth_enable": "启用 /ws/* 鉴权" "ws_auth_enable": "启用 /ws/* 鉴权"
}, },

View File

@@ -288,12 +288,6 @@ export function DashboardPage() {
{config.loggingToFile ? t('common.yes') : t('common.no')} {config.loggingToFile ? t('common.yes') : t('common.no')}
</span> </span>
</div> </div>
<div className={styles.configItem}>
<span className={styles.configLabel}>{t('basic_settings.request_log_enable')}</span>
<span className={`${styles.configValue} ${config.requestLog ? styles.enabled : styles.disabled}`}>
{config.requestLog ? t('common.yes') : t('common.no')}
</span>
</div>
<div className={styles.configItem}> <div className={styles.configItem}>
<span className={styles.configLabel}>{t('basic_settings.retry_count_label')}</span> <span className={styles.configLabel}>{t('basic_settings.retry_count_label')}</span>
<span className={styles.configValue}>{config.requestRetry ?? 0}</span> <span className={styles.configValue}>{config.requestRetry ?? 0}</span>

View File

@@ -16,7 +16,6 @@ type PendingKey =
| 'switchProject' | 'switchProject'
| 'switchPreview' | 'switchPreview'
| 'usage' | 'usage'
| 'requestLog'
| 'loggingToFile' | 'loggingToFile'
| 'wsAuth'; | 'wsAuth';
@@ -70,7 +69,7 @@ export function SettingsPage() {
const toggleSetting = async ( const toggleSetting = async (
section: PendingKey, section: PendingKey,
rawKey: 'debug' | 'usage-statistics-enabled' | 'request-log' | 'logging-to-file' | 'ws-auth', rawKey: 'debug' | 'usage-statistics-enabled' | 'logging-to-file' | 'ws-auth',
value: boolean, value: boolean,
updater: (val: boolean) => Promise<any>, updater: (val: boolean) => Promise<any>,
successMessage: string successMessage: string
@@ -81,8 +80,6 @@ export function SettingsPage() {
return config?.debug ?? false; return config?.debug ?? false;
case 'usage-statistics-enabled': case 'usage-statistics-enabled':
return config?.usageStatisticsEnabled ?? false; return config?.usageStatisticsEnabled ?? false;
case 'request-log':
return config?.requestLog ?? false;
case 'logging-to-file': case 'logging-to-file':
return config?.loggingToFile ?? false; return config?.loggingToFile ?? false;
case 'ws-auth': case 'ws-auth':
@@ -200,21 +197,6 @@ export function SettingsPage() {
} }
/> />
<ToggleSwitch
label={t('basic_settings.request_log_enable')}
checked={config?.requestLog ?? false}
disabled={disableControls || pending.requestLog || loading}
onChange={(value) =>
toggleSetting(
'requestLog',
'request-log',
value,
configApi.updateRequestLog,
t('notification.request_log_updated')
)
}
/>
<ToggleSwitch <ToggleSwitch
label={t('basic_settings.logging_to_file_enable')} label={t('basic_settings.logging_to_file_enable')}
checked={config?.loggingToFile ?? false} checked={config?.loggingToFile ?? false}

View File

@@ -308,6 +308,12 @@ textarea {
} }
} }
.switch-label-left {
.label {
order: -1;
}
}
.pill { .pill {
padding: 4px 10px; padding: 4px 10px;
border-radius: $radius-full; border-radius: $radius-full;
@@ -410,6 +416,17 @@ textarea {
background: var(--bg-primary); background: var(--bg-primary);
} }
.request-log-modal {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $spacing-md;
.status-badge {
margin-bottom: 0;
}
}
.empty-state { .empty-state {
border: 1px dashed var(--border-color); border: 1px dashed var(--border-color);
border-radius: $radius-lg; border-radius: $radius-lg;