mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat: add fullscreen logs view
This commit is contained in:
@@ -182,6 +182,28 @@ export function IconTrash2({ size = 20, ...props }: IconProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function IconMaximize2({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M15 3h6v6" />
|
||||
<path d="m21 3-7 7" />
|
||||
<path d="M9 21H3v-6" />
|
||||
<path d="m3 21 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconMinimize2({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M4 14h6v6" />
|
||||
<path d="m10 14-7 7" />
|
||||
<path d="M20 10h-6V4" />
|
||||
<path d="m14 10 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPlus({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||
|
||||
@@ -732,6 +732,8 @@
|
||||
"title": "Logs Viewer",
|
||||
"refresh_button": "Refresh Logs",
|
||||
"clear_button": "Clear Logs",
|
||||
"fullscreen_button": "Full Screen",
|
||||
"exit_fullscreen_button": "Exit Full Screen",
|
||||
"download_button": "Download Logs",
|
||||
"error_log_button": "Select Error Log",
|
||||
"error_logs_modal_title": "Error Request Logs",
|
||||
|
||||
@@ -729,6 +729,8 @@
|
||||
"title": "Просмотр журналов",
|
||||
"refresh_button": "Обновить журналы",
|
||||
"clear_button": "Очистить журналы",
|
||||
"fullscreen_button": "На весь экран",
|
||||
"exit_fullscreen_button": "Выйти из полноэкранного режима",
|
||||
"download_button": "Скачать журналы",
|
||||
"error_log_button": "Выбрать журнал ошибок",
|
||||
"error_logs_modal_title": "Журналы ошибок запросов",
|
||||
|
||||
@@ -732,6 +732,8 @@
|
||||
"title": "日志查看",
|
||||
"refresh_button": "刷新日志",
|
||||
"clear_button": "清空日志",
|
||||
"fullscreen_button": "全屏显示",
|
||||
"exit_fullscreen_button": "退出全屏",
|
||||
"download_button": "下载日志",
|
||||
"error_log_button": "选择错误日志",
|
||||
"error_logs_modal_title": "错误请求日志",
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
"title": "記錄檢視",
|
||||
"refresh_button": "重新整理記錄",
|
||||
"clear_button": "清空記錄",
|
||||
"fullscreen_button": "全螢幕顯示",
|
||||
"exit_fullscreen_button": "退出全螢幕",
|
||||
"download_button": "下載記錄",
|
||||
"error_log_button": "選擇錯誤記錄",
|
||||
"error_logs_modal_title": "錯誤請求記錄",
|
||||
|
||||
@@ -722,3 +722,47 @@
|
||||
height: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.logCardFullscreen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: $z-modal;
|
||||
min-height: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding: $spacing-lg;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
|
||||
@include mobile {
|
||||
padding: $spacing-md;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.logs-fullscreen-active) :global(.page-transition__layer) {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.logPanelFullscreen {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
resize: none;
|
||||
|
||||
@include tablet {
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
+118
-57
@@ -7,12 +7,15 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import { lockScroll, unlockScroll } from '@/components/ui/scrollLock';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconCode,
|
||||
IconDownload,
|
||||
IconEyeOff,
|
||||
IconMaximize2,
|
||||
IconMinimize2,
|
||||
IconRefreshCw,
|
||||
IconSearch,
|
||||
IconSlidersHorizontal,
|
||||
@@ -91,6 +94,7 @@ export function LogsPage() {
|
||||
const [errorLogsError, setErrorLogsError] = useState('');
|
||||
const [requestLogId, setRequestLogId] = useState<string | null>(null);
|
||||
const [requestLogDownloading, setRequestLogDownloading] = useState(false);
|
||||
const [fullscreenLogs, setFullscreenLogs] = useState(false);
|
||||
|
||||
const logScrollerRef = useRef<ReturnType<typeof useLogScroller> | null>(null);
|
||||
const longPressRef = useRef<{
|
||||
@@ -450,6 +454,27 @@ export function LogsPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fullscreenLogs) return;
|
||||
|
||||
document.body.classList.add('logs-fullscreen-active');
|
||||
lockScroll();
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setFullscreenLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.body.classList.remove('logs-fullscreen-active');
|
||||
unlockScroll();
|
||||
};
|
||||
}, [fullscreenLogs]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
||||
@@ -465,7 +490,10 @@ export function LogsPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.tabItem} ${activeTab === 'errors' ? styles.tabActive : ''}`}
|
||||
onClick={() => setActiveTab('errors')}
|
||||
onClick={() => {
|
||||
setFullscreenLogs(false);
|
||||
setActiveTab('errors');
|
||||
}}
|
||||
>
|
||||
{t('logs.error_logs_modal_title')}
|
||||
</button>
|
||||
@@ -473,67 +501,75 @@ export function LogsPage() {
|
||||
|
||||
<div className={styles.content}>
|
||||
{activeTab === 'logs' && (
|
||||
<Card className={styles.logCard}>
|
||||
<Card
|
||||
className={[styles.logCard, fullscreenLogs ? styles.logCardFullscreen : '']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
<div className={styles.filters}>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('logs.search_placeholder')}
|
||||
className={styles.searchInput}
|
||||
rightElement={
|
||||
searchQuery ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.searchClear}
|
||||
onClick={() => setSearchQuery('')}
|
||||
title="Clear"
|
||||
aria-label="Clear"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<IconSearch size={16} className={styles.searchIcon} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!fullscreenLogs && (
|
||||
<>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('logs.search_placeholder')}
|
||||
className={styles.searchInput}
|
||||
rightElement={
|
||||
searchQuery ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.searchClear}
|
||||
onClick={() => setSearchQuery('')}
|
||||
title="Clear"
|
||||
aria-label="Clear"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<IconSearch size={16} className={styles.searchIcon} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterPanelHeader}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.filterPanelToggle}
|
||||
onClick={() => setStructuredFiltersExpanded((prev) => !prev)}
|
||||
aria-expanded={structuredFiltersExpanded}
|
||||
aria-controls={structuredFiltersPanelId}
|
||||
title={
|
||||
structuredFiltersExpanded
|
||||
? t('logs.filter_panel_collapse')
|
||||
: t('logs.filter_panel_expand')
|
||||
}
|
||||
>
|
||||
<span className={styles.filterPanelButtonContent}>
|
||||
<IconSlidersHorizontal size={16} />
|
||||
<span>{t('logs.filter_panel_title')}</span>
|
||||
{structuredFilterCount > 0 && (
|
||||
<span className={styles.filterPanelCount}>
|
||||
{t('logs.filter_panel_active_count', { count: structuredFilterCount })}
|
||||
<div className={styles.filterPanelHeader}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.filterPanelToggle}
|
||||
onClick={() => setStructuredFiltersExpanded((prev) => !prev)}
|
||||
aria-expanded={structuredFiltersExpanded}
|
||||
aria-controls={structuredFiltersPanelId}
|
||||
title={
|
||||
structuredFiltersExpanded
|
||||
? t('logs.filter_panel_collapse')
|
||||
: t('logs.filter_panel_expand')
|
||||
}
|
||||
>
|
||||
<span className={styles.filterPanelButtonContent}>
|
||||
<IconSlidersHorizontal size={16} />
|
||||
<span>{t('logs.filter_panel_title')}</span>
|
||||
{structuredFilterCount > 0 && (
|
||||
<span className={styles.filterPanelCount}>
|
||||
{t('logs.filter_panel_active_count', { count: structuredFilterCount })}
|
||||
</span>
|
||||
)}
|
||||
{structuredFiltersExpanded ? (
|
||||
<IconChevronUp size={16} />
|
||||
) : (
|
||||
<IconChevronDown size={16} />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{structuredFiltersExpanded ? (
|
||||
<IconChevronUp size={16} />
|
||||
) : (
|
||||
<IconChevronDown size={16} />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{structuredFiltersExpanded && (
|
||||
{!fullscreenLogs && structuredFiltersExpanded && (
|
||||
<div id={structuredFiltersPanelId} className={styles.structuredFilters}>
|
||||
<div className={styles.filterChipGroup}>
|
||||
<span className={styles.filterChipLabel}>{t('logs.filter_method')}</span>
|
||||
@@ -690,6 +726,29 @@ export function LogsPage() {
|
||||
{t('logs.clear_button')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setFullscreenLogs((prev) => !prev)}
|
||||
className={styles.actionButton}
|
||||
aria-pressed={fullscreenLogs}
|
||||
title={
|
||||
fullscreenLogs
|
||||
? t('logs.exit_fullscreen_button')
|
||||
: t('logs.fullscreen_button')
|
||||
}
|
||||
>
|
||||
<span className={styles.buttonContent}>
|
||||
{fullscreenLogs ? (
|
||||
<IconMinimize2 size={16} />
|
||||
) : (
|
||||
<IconMaximize2 size={16} />
|
||||
)}
|
||||
{fullscreenLogs
|
||||
? t('logs.exit_fullscreen_button')
|
||||
: t('logs.fullscreen_button')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -698,7 +757,9 @@ export function LogsPage() {
|
||||
) : logState.buffer.length > 0 && filteredLines.length > 0 ? (
|
||||
<div
|
||||
ref={scroller.logViewerRef}
|
||||
className={styles.logPanel}
|
||||
className={[styles.logPanel, fullscreenLogs ? styles.logPanelFullscreen : '']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onScroll={scroller.handleLogScroll}
|
||||
>
|
||||
{scroller.canLoadMore && (
|
||||
|
||||
Reference in New Issue
Block a user