feat: 实现图片压缩功能
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user