diff --git a/frontend/src/components/SystemMonitor.tsx b/frontend/src/components/SystemMonitor.tsx index 93ca475..6b61328 100644 --- a/frontend/src/components/SystemMonitor.tsx +++ b/frontend/src/components/SystemMonitor.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { getSystemOverview, readFile, writeFile, SystemStats, NetworkInfo, ProcessInfo, DiskInfo } from "../lib/api"; +import { downloadFile, getSystemOverview, readFile, writeFile, SystemStats, NetworkInfo, ProcessInfo, DiskInfo } from "../lib/api"; import { useApp } from "../context/AppContext"; import type { NetworkProfile } from "../context/AppContext"; import { useToast } from "./Toast"; @@ -27,6 +27,16 @@ function formatSpeed(bytesPerSec: number): string { return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } +const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "ico", "tif", "tiff"]); + +function isImagePath(path: string): boolean { + const normalized = path.split("?")[0].split("#")[0]; + const lastDot = normalized.lastIndexOf("."); + if (lastDot === -1) return false; + const ext = normalized.slice(lastDot + 1).toLowerCase(); + return IMAGE_EXTENSIONS.has(ext); +} + type ProgressBarProps = { percent: number; isDark: boolean; @@ -85,6 +95,8 @@ export function SystemMonitor({ sessionId, isDark, selectedFilePath, networkProf const [disks, setDisks] = React.useState([]); const [fileContent, setFileContent] = React.useState(""); const [originalContent, setOriginalContent] = React.useState(""); + const [imageUrl, setImageUrl] = React.useState(null); + const [imageError, setImageError] = React.useState(null); const [fileLoading, setFileLoading] = React.useState(false); const [fileTooLarge, setFileTooLarge] = React.useState(false); const [fileSize, setFileSize] = React.useState(0); @@ -116,15 +128,83 @@ export function SystemMonitor({ sessionId, isDark, selectedFilePath, networkProf } }, [sessionId, t]); - // 读取文件内容 + const isImageFile = React.useMemo(() => { + return selectedFilePath ? isImagePath(selectedFilePath) : false; + }, [selectedFilePath]); + + const revokeImageUrl = React.useCallback((url: string | null) => { + if (url) { + URL.revokeObjectURL(url); + } + }, []); + + // 读取文件内容或加载图片 React.useEffect(() => { + let cancelled = false; + if (!selectedFilePath) { setFileContent(""); setOriginalContent(""); setFileTooLarge(false); - return; + setImageError(null); + setFileSize(0); + setImageUrl((prev) => { + revokeImageUrl(prev); + return null; + }); + return () => { + cancelled = true; + }; } + if (isImageFile) { + setFileLoading(true); + setFileTooLarge(false); + setFileContent(""); + setOriginalContent(""); + setImageError(null); + + const loadImage = async () => { + try { + const blob = await downloadFile(sessionId, selectedFilePath); + if (cancelled) { + return; + } + const url = URL.createObjectURL(blob); + setImageUrl((prev) => { + revokeImageUrl(prev); + return url; + }); + setFileSize(blob.size); + } catch (err) { + if (cancelled) { + return; + } + setImageError(err instanceof Error ? err.message : t("图片加载失败", "Failed to load image")); + setImageUrl((prev) => { + revokeImageUrl(prev); + return null; + }); + } finally { + if (!cancelled) { + setFileLoading(false); + } + } + }; + + loadImage(); + + return () => { + cancelled = true; + }; + } + + setImageError(null); + setImageUrl((prev) => { + revokeImageUrl(prev); + return null; + }); + const loadFile = async (force = false) => { setFileLoading(true); try { @@ -144,15 +224,28 @@ export function SystemMonitor({ sessionId, isDark, selectedFilePath, networkProf setFileContent(""); setOriginalContent(""); } finally { - setFileLoading(false); + if (!cancelled) { + setFileLoading(false); + } } }; loadFile(); - }, [sessionId, selectedFilePath, push, t]); + + return () => { + cancelled = true; + }; + }, [sessionId, selectedFilePath, isImageFile, push, t, revokeImageUrl, downloadFile]); + + React.useEffect(() => { + return () => { + revokeImageUrl(imageUrl); + }; + }, [imageUrl, revokeImageUrl]); const handleLoadLargeFile = React.useCallback(async () => { if (!selectedFilePath) return; + if (isImageFile) return; setFileLoading(true); try { const result = await readFile(sessionId, selectedFilePath, true); @@ -164,7 +257,7 @@ export function SystemMonitor({ sessionId, isDark, selectedFilePath, networkProf } finally { setFileLoading(false); } - }, [sessionId, selectedFilePath, push, t]); + }, [sessionId, selectedFilePath, push, t, isImageFile]); // 保存文件 const handleSaveFile = React.useCallback(async () => { @@ -420,46 +513,68 @@ export function SystemMonitor({ sessionId, isDark, selectedFilePath, networkProf {selectedFilePath && (
{isCompact ? ( - renderSectionHeader("file", t("文件预览", "File Preview")) + renderSectionHeader("file", isImageFile ? t("图片预览", "Image Preview") : t("文件预览", "File Preview")) ) : (

- {t("文件预览", "File Preview")} + {isImageFile ? t("图片预览", "Image Preview") : t("文件预览", "File Preview")}

- + {!isImageFile ? ( + + ) : null}
)} {!isCompact || !collapsedSections.file ? ( <> {isCompact ? (
- + {!isImageFile ? ( + + ) : null}
) : null} {fileLoading ? (
⟳ {t("加载中...", "Loading...")}
+ ) : isImageFile ? ( + imageError ? ( +
+ {imageError} +
+ ) : imageUrl ? ( +
+ {selectedFilePath} +
+ ) : ( +
+ {t("暂无图片内容", "No image preview")} +
+ ) ) : fileTooLarge ? (

{t(`文件过大 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`, `File too large (${(fileSize / 1024 / 1024).toFixed(2)} MB)`)}