feat: 同步上游更新 - 可视化配置编辑器与导航重构
- feat(config): 添加可视化配置编辑器和 YAML 处理 - feat(i18n): 可视化配置编辑器国际化支持 - feat(logs): 优化日志加载,支持自动预加载 - feat(ui): API 使用统计显示成功/失败状态 - feat(ui): Payload 规则编辑器空状态显示 - refactor(nav): Config Panel 导航重构,移除 Settings/API Keys 页面 - fix(ui): Config Panel 操作栏居中,ProviderNav 移至底部 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
@@ -643,6 +643,29 @@ export function LogsPage() {
|
||||
|
||||
const canLoadMore = !isSearching && logState.visibleFrom > 0;
|
||||
|
||||
const prependVisibleLines = useCallback(() => {
|
||||
const node = logViewerRef.current;
|
||||
if (!node) return;
|
||||
if (pendingPrependScrollRef.current) return;
|
||||
if (isSearching) return;
|
||||
|
||||
setLogState((prev) => {
|
||||
if (prev.visibleFrom <= 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
pendingPrependScrollRef.current = {
|
||||
scrollHeight: node.scrollHeight,
|
||||
scrollTop: node.scrollTop,
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
visibleFrom: Math.max(prev.visibleFrom - LOAD_MORE_LINES, 0),
|
||||
};
|
||||
});
|
||||
}, [isSearching]);
|
||||
|
||||
const handleLogScroll = () => {
|
||||
const node = logViewerRef.current;
|
||||
if (!node) return;
|
||||
@@ -651,14 +674,7 @@ export function LogsPage() {
|
||||
if (pendingPrependScrollRef.current) return;
|
||||
if (node.scrollTop > LOAD_MORE_THRESHOLD_PX) return;
|
||||
|
||||
pendingPrependScrollRef.current = {
|
||||
scrollHeight: node.scrollHeight,
|
||||
scrollTop: node.scrollTop,
|
||||
};
|
||||
setLogState((prev) => ({
|
||||
...prev,
|
||||
visibleFrom: Math.max(prev.visibleFrom - LOAD_MORE_LINES, 0),
|
||||
}));
|
||||
prependVisibleLines();
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -671,6 +687,53 @@ export function LogsPage() {
|
||||
pendingPrependScrollRef.current = null;
|
||||
}, [logState.visibleFrom]);
|
||||
|
||||
const tryAutoLoadMoreUntilScrollable = useCallback(() => {
|
||||
const node = logViewerRef.current;
|
||||
if (!node) return;
|
||||
if (!canLoadMore) return;
|
||||
if (isSearching) return;
|
||||
if (pendingPrependScrollRef.current) return;
|
||||
|
||||
const hasVerticalOverflow = node.scrollHeight > node.clientHeight + 1;
|
||||
if (hasVerticalOverflow) return;
|
||||
|
||||
prependVisibleLines();
|
||||
}, [canLoadMore, isSearching, prependVisibleLines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (activeTab !== 'logs') return;
|
||||
|
||||
const raf = window.requestAnimationFrame(() => {
|
||||
tryAutoLoadMoreUntilScrollable();
|
||||
});
|
||||
return () => {
|
||||
window.cancelAnimationFrame(raf);
|
||||
};
|
||||
}, [
|
||||
activeTab,
|
||||
loading,
|
||||
tryAutoLoadMoreUntilScrollable,
|
||||
filteredLines.length,
|
||||
showRawLogs,
|
||||
logState.visibleFrom,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'logs') return;
|
||||
|
||||
const onResize = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
tryAutoLoadMoreUntilScrollable();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, [activeTab, tryAutoLoadMoreUntilScrollable]);
|
||||
|
||||
const copyLogLine = async (raw: string) => {
|
||||
const ok = await copyToClipboard(raw);
|
||||
if (ok) {
|
||||
|
||||
Reference in New Issue
Block a user