feat: 实现图片压缩功能

This commit is contained in:
2026-01-25 22:37:40 +08:00
parent 081e2058bf
commit 54009163b1
18 changed files with 1603 additions and 167 deletions

View File

@@ -14,7 +14,7 @@ import { useTranslation, getServerTranslations } from "@/lib/i18n";
import type { UploadedFile, ProcessedFile, ImageCompressConfig } from "@/types";
const imageAccept = {
"image/*": [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"],
"image/*": [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff"],
};
const defaultConfig: ImageCompressConfig = {
@@ -52,6 +52,51 @@ function useConfigOptions(config: ImageCompressConfig, getT: (key: string) => st
];
}
/**
* Upload a file to the server
*/
async function uploadFile(file: File): Promise<{ fileId: string } | null> {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Upload failed");
}
const data = await response.json();
return { fileId: data.fileId };
}
/**
* Process image compression
*/
async function processImageCompression(
fileId: string,
config: ImageCompressConfig
): Promise<{ success: boolean; data?: any; error?: string }> {
const response = await fetch("/api/process/image-compress", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ fileId, config }),
});
const data = await response.json();
if (!response.ok) {
return { success: false, error: data.error || "Processing failed" };
}
return { success: true, data };
}
export default function ImageCompressPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
@@ -101,54 +146,99 @@ export default function ImageCompressPage() {
message: getT("processing.uploadingImages"),
});
const results: ProcessedFile[] = [];
const errors: string[] = [];
try {
// Simulate upload
for (let i = 0; i <= 100; i += 10) {
await new Promise((resolve) => setTimeout(resolve, 50));
// Process each file
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Update progress
const uploadProgress = Math.round(((i + 0.5) / files.length) * 50);
setProcessingStatus({
status: "uploading",
progress: i,
message: getT("processing.uploadProgress", { progress: i }),
progress: uploadProgress,
message: getT("processing.uploadProgress", { progress: uploadProgress }),
});
}
setProcessingStatus({
status: "processing",
progress: 0,
message: getT("processing.compressingImages"),
});
// Upload file
let fileId: string;
try {
const uploadResult = await uploadFile(file.file);
if (!uploadResult) {
throw new Error("Upload failed");
}
fileId = uploadResult.fileId;
} catch (error) {
errors.push(`${file.name}: ${error instanceof Error ? error.message : "Upload failed"}`);
continue;
}
// Simulate processing
for (let i = 0; i <= 100; i += 5) {
await new Promise((resolve) => setTimeout(resolve, 100));
// Update progress to processing
const processProgress = 50 + Math.round(((i + 0.5) / files.length) * 50);
setProcessingStatus({
status: "processing",
progress: i,
message: getT("processing.compressProgress", { progress: i }),
progress: processProgress,
message: getT("processing.compressProgress", { progress: processProgress }),
});
// Process image
try {
const result = await processImageCompression(fileId, config);
if (result.success && result.data) {
results.push({
id: generateId(),
originalFile: file,
processedUrl: result.data.fileUrl,
metadata: {
format: result.data.metadata.format,
quality: result.data.metadata.quality,
compressionRatio: result.data.metadata.compressionRatio,
resolution: `${result.data.metadata.width}x${result.data.metadata.height}`,
originalSize: result.data.metadata.originalSize,
compressedSize: result.data.metadata.compressedSize,
},
createdAt: new Date(),
});
} else {
errors.push(
`${file.name}: ${result.error || getT("processing.unknownError")}`
);
}
} catch (error) {
errors.push(
`${file.name}: ${error instanceof Error ? error.message : "Processing failed"}`
);
}
}
// Simulate completion
const results: ProcessedFile[] = files.map((file) => ({
id: generateId(),
originalFile: file,
processedUrl: "#",
metadata: {
format: config.format === "original" ? file.file.type.split("/")[1] : config.format,
quality: config.quality,
compressionRatio: Math.floor(Math.random() * 30) + 40, // Simulated 40-70%
},
createdAt: new Date(),
}));
setProcessedFiles(results);
// Clear uploaded files
clearFiles();
setProcessingStatus({
status: "completed",
progress: 100,
message: getT("processing.compressionComplete"),
});
// Set final status
if (results.length > 0) {
setProcessedFiles(results);
setProcessingStatus({
status: "completed",
progress: 100,
message: getT("processing.compressionComplete"),
});
} else if (errors.length > 0) {
setProcessingStatus({
status: "failed",
progress: 0,
message: errors[0],
error: errors.join("; "),
});
} else {
setProcessingStatus({
status: "failed",
progress: 0,
message: getT("processing.compressionFailed"),
});
}
} catch (error) {
setProcessingStatus({
status: "failed",
@@ -160,7 +250,16 @@ export default function ImageCompressPage() {
};
const handleDownload = (fileId: string) => {
console.log("Downloading file:", fileId);
const file = processedFiles.find((f) => f.id === fileId);
if (file) {
// Create a temporary link to trigger download
const link = document.createElement("a");
link.href = file.processedUrl;
link.download = file.metadata.filename || `compressed-${file.originalFile.name}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const canProcess = files.length > 0 && processingStatus.status !== "processing";
@@ -168,10 +267,7 @@ export default function ImageCompressPage() {
return (
<div className="p-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<div className="mb-8">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
@@ -229,9 +325,11 @@ export default function ImageCompressPage() {
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
<h3 className="mb-3 font-semibold">{getT("tools.imageCompression.features")}</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
{(getT("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
<li key={index}> {feature}</li>
))}
{(getT("tools.imageCompression.featureList") as unknown as string[]).map(
(feature, index) => (
<li key={index}> {feature}</li>
)
)}
</ul>
</div>
</div>