diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index d512d95..1108cac 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -146,7 +146,7 @@ "excluded_models_label": "排除的模型 (可选):", "excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash", "excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。", - "excluded_models_count": "排除 {count} 个模型", + "excluded_models_count": "排除 {{count}} 个模型", "codex_title": "Codex API 配置", "codex_add_button": "添加配置", "codex_empty_title": "暂无Codex配置", @@ -219,7 +219,7 @@ "openai_models_search_placeholder": "按名称、别名或描述筛选", "openai_models_search_empty": "没有匹配的模型,请更换关键字试试。", "openai_models_fetch_invalid_url": "请先填写有效的 Base URL", - "openai_models_fetch_added": "已添加 {count} 个新模型", + "openai_models_fetch_added": "已添加 {{count}} 个新模型", "openai_edit_modal_title": "编辑OpenAI兼容提供商", "openai_edit_modal_name_label": "提供商名称:", "openai_edit_modal_url_label": "Base URL:", @@ -253,19 +253,19 @@ "delete_button": "删除", "delete_confirm": "确定要删除文件", "delete_all_confirm": "确定要删除所有认证文件吗?此操作不可恢复!", - "delete_filtered_confirm": "确定要删除筛选出的 {type} 认证文件吗?此操作不可恢复!", + "delete_filtered_confirm": "确定要删除筛选出的 {{type}} 认证文件吗?此操作不可恢复!", "upload_error_json": "只能上传JSON文件", "upload_success": "文件上传成功", "download_success": "文件下载成功", "delete_success": "文件删除成功", "delete_all_success": "成功删除", - "delete_filtered_success": "成功删除 {count} 个 {type} 认证文件", - "delete_filtered_partial": "{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个", - "delete_filtered_none": "当前筛选类型 ({type}) 下没有可删除的认证文件", + "delete_filtered_success": "成功删除 {{count}} 个 {{type}} 认证文件", + "delete_filtered_partial": "{{type}} 认证文件删除完成,成功 {{success}} 个,失败 {{failed}} 个", + "delete_filtered_none": "当前筛选类型 ({{type}}) 下没有可删除的认证文件", "files_count": "个文件", "pagination_prev": "上一页", "pagination_next": "下一页", - "pagination_info": "第 {current} / {total} 页 · 共 {count} 个文件", + "pagination_info": "第 {{current}} / {{total}} 页 · 共 {{count}} 个文件", "search_label": "搜索配置文件", "search_placeholder": "输入名称、类型或提供方关键字", "page_size_label": "单页数量", @@ -318,7 +318,7 @@ "description": "按提供商分列展示,点击卡片编辑或删除;支持 * 通配符,范围跟随上方的配置文件过滤标签。", "add": "新增排除", "add_title": "新增提供商排除列表", - "edit_title": "编辑 {provider} 的排除列表", + "edit_title": "编辑 {{provider}} 的排除列表", "refresh": "刷新", "refreshing": "刷新中...", "provider_label": "提供商", @@ -333,19 +333,19 @@ "save_success": "排除列表已更新", "save_failed": "更新排除列表失败", "delete": "删除提供商", - "delete_confirm": "确定要删除 {provider} 的排除列表吗?", + "delete_confirm": "确定要删除 {{provider}} 的排除列表吗?", "delete_success": "已删除该提供商的排除列表", "delete_failed": "删除排除列表失败", "deleting": "正在删除...", "no_models": "未配置排除模型", - "model_count": "排除 {count} 个模型", + "model_count": "排除 {{count}} 个模型", "list_empty_all": "暂无任何提供商的排除列表,点击“新增排除”创建。", "list_empty_filtered": "当前筛选下没有排除项,点击“新增排除”添加。", "disconnected": "请先连接服务器以查看排除列表", "load_failed": "加载排除列表失败", "provider_required": "请先填写提供商名称", "scope_all": "当前范围:全局(显示所有提供商)", - "scope_provider": "当前范围:{provider}" + "scope_provider": "当前范围:{{provider}}" }, "auth_login": { "codex_oauth_title": "Codex OAuth", @@ -562,7 +562,7 @@ "models_loading": "正在加载可用模型...", "models_empty": "未从 /v1/models 获取到模型数据", "models_error": "获取模型列表失败", - "models_count": "可用模型 {count} 个", + "models_count": "可用模型 {{count}} 个", "version_check_title": "版本检查", "version_check_desc": "调用 /latest-version 接口比对服务器版本,提示是否有可用更新。", "version_current_label": "当前版本", @@ -570,7 +570,7 @@ "version_check_button": "检查更新", "version_check_idle": "点击检查更新", "version_checking": "正在检查最新版本...", - "version_update_available": "有新版本可用:{version}", + "version_update_available": "有新版本可用:{{version}}", "version_is_latest": "当前已是最新版本", "version_check_error": "检查更新失败", "version_current_missing": "未获取到服务器版本号,暂无法比对", @@ -595,7 +595,7 @@ "gemini_key_deleted": "Gemini密钥删除成功", "gemini_multi_input_required": "请先输入至少一个Gemini密钥", "gemini_multi_failed": "Gemini密钥批量添加失败", - "gemini_multi_summary": "Gemini批量添加完成:成功 {success},跳过 {skipped},失败 {failed}", + "gemini_multi_summary": "Gemini批量添加完成:成功 {{success}},跳过 {{skipped}},失败 {{failed}}", "codex_config_added": "Codex配置添加成功", "codex_config_updated": "Codex配置更新成功", "codex_config_deleted": "Codex配置删除成功", diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index ab50f93..87d0ab3 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; @@ -12,38 +12,69 @@ interface ErrorLogItem { modified?: number; } +// 限制显示的最大日志行数,防止渲染过多导致卡死 +const MAX_DISPLAY_LINES = 500; + export function LogsPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); - const [logs, setLogs] = useState(''); + const [logLines, setLogLines] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [autoRefresh, setAutoRefresh] = useState(false); - const [intervalId, setIntervalId] = useState(null); const [errorLogs, setErrorLogs] = useState([]); const [loadingErrors, setLoadingErrors] = useState(false); + // 保存最新时间戳用于增量获取 + const latestTimestampRef = useRef(0); + const disableControls = connectionStatus !== 'connected'; - const loadLogs = async () => { + const loadLogs = async (incremental = false) => { if (connectionStatus !== 'connected') { setLoading(false); return; } - setLoading(true); + if (!incremental) { + setLoading(true); + } setError(''); + try { - const data = await logsApi.fetchLogs({ limit: 500 }); - const text = Array.isArray(data) ? data.join('\n') : data?.logs || data || ''; - setLogs(text); + const params = incremental && latestTimestampRef.current > 0 + ? { after: latestTimestampRef.current } + : {}; + const data = await logsApi.fetchLogs(params); + + // 更新时间戳 + if (data['latest-timestamp']) { + latestTimestampRef.current = data['latest-timestamp']; + } + + const newLines = Array.isArray(data.lines) ? data.lines : []; + + if (incremental && newLines.length > 0) { + // 增量更新:追加新日志并限制总行数 + setLogLines(prev => { + const combined = [...prev, ...newLines]; + return combined.slice(-MAX_DISPLAY_LINES); + }); + } else if (!incremental) { + // 全量加载:只取最后 MAX_DISPLAY_LINES 行 + setLogLines(newLines.slice(-MAX_DISPLAY_LINES)); + } } catch (err: any) { console.error('Failed to load logs:', err); - setError(err?.message || t('logs.load_error')); + if (!incremental) { + setError(err?.message || t('logs.load_error')); + } } finally { - setLoading(false); + if (!incremental) { + setLoading(false); + } } }; @@ -51,7 +82,8 @@ export function LogsPage() { if (!window.confirm(t('logs.clear_confirm'))) return; try { await logsApi.clearLogs(); - setLogs(''); + setLogLines([]); + latestTimestampRef.current = 0; showNotification(t('logs.clear_success'), 'success'); } catch (err: any) { showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); @@ -59,7 +91,8 @@ export function LogsPage() { }; const downloadLogs = () => { - const blob = new Blob([logs], { type: 'text/plain' }); + const text = logLines.join('\n'); + const blob = new Blob([text], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -78,13 +111,15 @@ export function LogsPage() { setLoadingErrors(true); try { const res = await logsApi.fetchErrorLogs(); - const list: ErrorLogItem[] = Array.isArray(res) - ? res - : Object.entries(res || {}).map(([name, meta]) => ({ - name, - size: (meta as any)?.size, - modified: (meta as any)?.modified - })); + // API 返回 { files: [...] } + const files = (res as any)?.files; + const list: ErrorLogItem[] = Array.isArray(files) + ? files.map((f: any) => ({ + name: f.name, + size: f.size, + modified: f.modified + })) + : []; setErrorLogs(list); } catch (err: any) { console.error('Failed to load error logs:', err); @@ -113,23 +148,25 @@ export function LogsPage() { useEffect(() => { if (connectionStatus === 'connected') { - loadLogs(); + latestTimestampRef.current = 0; + loadLogs(false); loadErrorLogs(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectionStatus]); useEffect(() => { - if (autoRefresh) { - const id = window.setInterval(loadLogs, 8000); - setIntervalId(id); - return () => window.clearInterval(id); + if (!autoRefresh || connectionStatus !== 'connected') { + return; } - if (intervalId) { - window.clearInterval(intervalId); - setIntervalId(null); - } - }, [autoRefresh]); + const id = window.setInterval(() => { + loadLogs(true); + }, 8000); + return () => window.clearInterval(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoRefresh, connectionStatus]); + + const logsText = logLines.join('\n'); return (
@@ -137,13 +174,13 @@ export function LogsPage() { title={t('logs.title')} extra={
- -