文件预览支持图片显示
This commit is contained in:
1 parent
e85d25199d
commit
17456095eb
1 file changed
+145
-30
@@ -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<DiskInfo[]>([]);
|
||||
const [fileContent, setFileContent] = React.useState<string>("");
|
||||
const [originalContent, setOriginalContent] = React.useState<string>("");
|
||||
const [imageUrl, setImageUrl] = React.useState<string | null>(null);
|
||||
const [imageError, setImageError] = React.useState<string | null>(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 && (
|
||||
<div className={`pt-4 ${isDark ? "border-t border-slate-700" : "border-t border-slate-200"}`}>
|
||||
{isCompact ? (
|
||||
renderSectionHeader("file", t("文件预览", "File Preview"))
|
||||
renderSectionHeader("file", isImageFile ? t("图片预览", "Image Preview") : t("文件预览", "File Preview"))
|
||||
) : (
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className={`text-sm font-semibold ${isDark ? "text-slate-200" : "text-slate-700"}`}>
|
||||
{t("文件预览", "File Preview")}
|
||||
{isImageFile ? t("图片预览", "Image Preview") : t("文件预览", "File Preview")}
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleSaveFile}
|
||||
disabled={fileContent === originalContent || fileTooLarge}
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
fileContent === originalContent || fileTooLarge
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: (isDark ? "bg-indigo-600 hover:bg-indigo-500" : "bg-indigo-500 hover:bg-indigo-400")
|
||||
} text-white`}
|
||||
>
|
||||
{t("保存", "Save")}
|
||||
</button>
|
||||
{!isImageFile ? (
|
||||
<button
|
||||
onClick={handleSaveFile}
|
||||
disabled={fileContent === originalContent || fileTooLarge}
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
fileContent === originalContent || fileTooLarge
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: (isDark ? "bg-indigo-600 hover:bg-indigo-500" : "bg-indigo-500 hover:bg-indigo-400")
|
||||
} text-white`}
|
||||
>
|
||||
{t("保存", "Save")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{!isCompact || !collapsedSections.file ? (
|
||||
<>
|
||||
{isCompact ? (
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={handleSaveFile}
|
||||
disabled={fileContent === originalContent || fileTooLarge}
|
||||
className={`px-3 py-2 text-sm rounded ${
|
||||
fileContent === originalContent || fileTooLarge
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: (isDark ? "bg-indigo-600 hover:bg-indigo-500" : "bg-indigo-500 hover:bg-indigo-400")
|
||||
} text-white`}
|
||||
>
|
||||
{t("保存", "Save")}
|
||||
</button>
|
||||
{!isImageFile ? (
|
||||
<button
|
||||
onClick={handleSaveFile}
|
||||
disabled={fileContent === originalContent || fileTooLarge}
|
||||
className={`px-3 py-2 text-sm rounded ${
|
||||
fileContent === originalContent || fileTooLarge
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: (isDark ? "bg-indigo-600 hover:bg-indigo-500" : "bg-indigo-500 hover:bg-indigo-400")
|
||||
} text-white`}
|
||||
>
|
||||
{t("保存", "Save")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{fileLoading ? (
|
||||
<div className={`flex items-center justify-center text-xs ${isDark ? "text-slate-400" : "text-slate-500"}`} style={{ height: "172px" }}>
|
||||
<span className="animate-spin mr-2">⟳</span> {t("加载中...", "Loading...")}
|
||||
</div>
|
||||
) : isImageFile ? (
|
||||
imageError ? (
|
||||
<div className={`flex items-center justify-center text-xs ${isDark ? "text-rose-400" : "text-rose-500"}`} style={{ height: "172px" }}>
|
||||
{imageError}
|
||||
</div>
|
||||
) : imageUrl ? (
|
||||
<div className={`flex items-center justify-center rounded border ${isDark ? "border-slate-700 bg-slate-900/40" : "border-slate-200 bg-slate-50"}`} style={{ height: "172px" }}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={selectedFilePath}
|
||||
className="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex items-center justify-center text-xs ${isDark ? "text-slate-400" : "text-slate-500"}`} style={{ height: "172px" }}>
|
||||
{t("暂无图片内容", "No image preview")}
|
||||
</div>
|
||||
)
|
||||
) : fileTooLarge ? (
|
||||
<div className={`flex flex-col items-center justify-center text-xs ${isDark ? "text-slate-400" : "text-slate-500"}`} style={{ height: "172px" }}>
|
||||
<p className="mb-2">{t(`文件过大 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`, `File too large (${(fileSize / 1024 / 1024).toFixed(2)} MB)`)}</p>
|
||||
|
||||
Reference in new issue
Block a user