feat(logs): redesign LogsPage with structured log parsing and virtual scrolling

- Add log line parser to extract timestamp, level, status code, latency, IP, HTTP method, and path
  - Implement virtual scrolling with load-more on scroll-up to handle large log files efficiently
  - Replace monolithic pre block with structured grid layout for better readability
  - Add visual badges for log levels and HTTP status codes with color-coded severity
  - Add IconRefreshCw icon component
  - Update ToggleSwitch to accept ReactNode as label
  - Fix fetchConfig calls to use default parameters consistently
  - Add request deduplication in useConfigStore to prevent duplicate /config API calls
  - Add i18n keys for load_more_hint and hidden_lines
This commit is contained in:
Supra4E8C
2025-12-15 17:37:09 +08:00
parent f17329b0ff
commit 4d898b3e20
10 changed files with 656 additions and 92 deletions

View File

@@ -27,6 +27,9 @@ interface ConfigState {
isCacheValid: (section?: RawConfigSection) => boolean;
}
let configRequestToken = 0;
let inFlightConfigRequest: { id: number; promise: Promise<Config> } | null = null;
const SECTION_KEYS: RawConfigSection[] = [
'debug',
'proxy-url',
@@ -102,13 +105,35 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
}
}
// section 缓存未命中但 full 缓存可用时,直接复用已获取到的配置,避免重复 /config 请求
if (!forceRefresh && section && isCacheValid()) {
const fullCached = cache.get('__full__');
if (fullCached?.data) {
return extractSectionValue(fullCached.data as Config, section);
}
}
// 同一时刻合并多个 /config 请求(如 StrictMode 或多个页面同时触发)
if (inFlightConfigRequest) {
const data = await inFlightConfigRequest.promise;
return section ? extractSectionValue(data, section) : data;
}
// 获取新数据
set({ loading: true, error: null });
const requestId = (configRequestToken += 1);
try {
const data = await configApi.getConfig();
const requestPromise = configApi.getConfig();
inFlightConfigRequest = { id: requestId, promise: requestPromise };
const data = await requestPromise;
const now = Date.now();
// 如果在请求过程中连接已被切换/登出,则忽略旧请求的结果,避免覆盖新会话的状态
if (requestId !== configRequestToken) {
return section ? extractSectionValue(data, section) : data;
}
// 更新缓存
const newCache = new Map(cache);
newCache.set('__full__', { data, timestamp: now });
@@ -127,11 +152,17 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
return section ? extractSectionValue(data, section) : data;
} catch (error: any) {
set({
error: error.message || 'Failed to fetch config',
loading: false
});
if (requestId === configRequestToken) {
set({
error: error.message || 'Failed to fetch config',
loading: false
});
}
throw error;
} finally {
if (inFlightConfigRequest?.id === requestId) {
inFlightConfigRequest = null;
}
}
},
@@ -206,11 +237,18 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
newCache.delete(section);
// 同时清除完整配置缓存
newCache.delete('__full__');
set({ cache: newCache });
return;
} else {
newCache.clear();
}
set({ cache: newCache });
// 清除全部缓存一般代表“切换连接/登出/全量刷新”,需要让 in-flight 的旧请求失效
configRequestToken += 1;
inFlightConfigRequest = null;
set({ config: null, cache: newCache, loading: false, error: null });
},
isCacheValid: (section) => {