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:
kongkongyo
2026-02-06 14:20:16 +08:00
parent 43ec39040d
commit e2afc17f22
23 changed files with 2650 additions and 1135 deletions

View File

@@ -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) {