diff --git a/app/api/resize/route.ts b/app/api/resize/route.ts new file mode 100644 index 0000000..bb2e178 --- /dev/null +++ b/app/api/resize/route.ts @@ -0,0 +1,199 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { uploadFile } from "@/lib/storage/service"; +import { db } from "@/lib/db"; +import { generatedImage } from "@/lib/db/schema"; + +/** + * 压缩图片生成缩略图(使用 sharp) + */ +async function createThumbnail(buffer: Buffer, mimeType: string): Promise { + try { + const sharp = await import("sharp"); + + // 创建缩略图(宽度 500px,高度按比例,质量 80%) + const thumbnailBuffer = await sharp.default(buffer) + .resize(500, null, { + fit: "inside", + withoutEnlargement: true, + }) + .png({ quality: 80 }) // PNG格式保持透明背景 + .toBuffer(); + + console.log(`[Resize] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`); + return thumbnailBuffer; + } catch (error) { + console.error("[Resize] Failed to create thumbnail:", error); + // 如果失败,返回原图 + return buffer; + } +} + +/** + * 将 base64 图片上传到 S3,返回缩略图和原图的 URL + */ +async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/png"): Promise<{ thumbnail: string; original: string }> { + try { + // 解析 base64 数据 + const buffer = Buffer.from(base64Data, "base64"); + + // 生成文件名 + const ext = mimeType.split("/")[1] || "png"; + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).slice(2, 8); + const thumbnailFilename = `resize-thumb-${timestamp}-${randomStr}.${ext}`; + const originalFilename = `resize-orig-${timestamp}-${randomStr}.${ext}`; + + console.log(`[Resize] Original size: ${buffer.length} bytes`); + + // 生成缩略图 + const thumbnailBuffer = await createThumbnail(buffer, mimeType); + + // 转换为 ArrayBuffer + const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( + thumbnailBuffer.byteOffset, + thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength + ) as ArrayBuffer; + const originalArrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + + // 并行上传缩略图和原图到 S3 + console.log(`[Resize] Uploading thumbnail and original to S3...`); + const [thumbnailResult, originalResult] = await Promise.all([ + uploadFile(thumbnailFilename, thumbnailArrayBuffer), + uploadFile(originalFilename, originalArrayBuffer), + ]); + + console.log(`[Resize] Uploaded to S3:`); + console.log(` - Thumbnail: ${thumbnailResult.url}`); + console.log(` - Original: ${originalResult.url}`); + + return { + thumbnail: thumbnailResult.url, + original: originalResult.url, + }; + } catch (error) { + console.error("[Resize] Failed to upload to S3:", error); + throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`); + } +} + +/** + * 尺寸调整API路由 + * 接收前端处理后的Base64图片,上传到S3并保存到数据库 + */ +export async function POST(request: NextRequest) { + try { + // 0. 获取用户会话 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json( + { error: "未授权,请先登录" }, + { status: 401 } + ); + } + + const userId = session.user.id; + + // 1. 获取请求体 + const contentType = request.headers.get("content-type") || ""; + + let imageBase64: string; + let prompt = "尺寸调整处理"; + let chatId: string | null = null; + let messageId: string | null = null; + let canvasWidth: number; + let canvasHeight: number; + let mimeType = "image/png"; + + if (contentType.includes("application/json")) { + const body = await request.json(); + imageBase64 = body.image_base64; + prompt = body.prompt || "尺寸调整处理"; + chatId = body.chatId || null; + messageId = body.messageId || null; + canvasWidth = body.canvasWidth; + canvasHeight = body.canvasHeight; + mimeType = body.mimeType || "image/png"; + + if (!imageBase64) { + return NextResponse.json( + { error: "缺少image_base64参数" }, + { status: 400 } + ); + } + + if (!canvasWidth || !canvasHeight) { + return NextResponse.json( + { error: "缺少canvasWidth或canvasHeight参数" }, + { status: 400 } + ); + } + } else { + return NextResponse.json( + { error: "不支持的Content-Type,请使用application/json" }, + { status: 400 } + ); + } + + console.log(`[Resize API] 图片Base64长度: ${imageBase64.length} 字符`); + console.log(`[Resize API] 画布尺寸: ${canvasWidth}x${canvasHeight}`); + + // 2. 上传处理后的图片到S3存储桶 + let thumbnailUrl: string; + let originalUrl: string; + + try { + const uploadResult = await uploadBase64ToS3(imageBase64, mimeType); + thumbnailUrl = uploadResult.thumbnail; + originalUrl = uploadResult.original; + console.log(`[Resize] ✅ Images uploaded to S3 successfully`); + } catch (uploadError) { + console.error("[Resize] ❌ Failed to upload to S3:", uploadError); + return NextResponse.json( + { + error: "图片上传到存储桶失败", + detail: uploadError instanceof Error ? uploadError.message : "未知错误" + }, + { status: 500 } + ); + } + + // 3. 保存图片记录到数据库(相册和生图记录) + try { + const [savedImage] = await db.insert(generatedImage).values({ + userId, + chatId: chatId || null, + messageId: messageId || null, + thumbnailUrl, + originalUrl, + imageSize: `${canvasWidth}x${canvasHeight}`, // 使用实际尺寸 + prompt: prompt || "尺寸调整处理", + modelId: "resize-1.0", // 标记为尺寸调整处理 + }).returning(); + console.log(`[Resize] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`); + } catch (dbError) { + console.error("[Resize] ❌ 保存到数据库失败:", dbError); + // 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问) + } + + // 4. 返回结果(包含S3 URL和Base64,前端可以选择使用) + return NextResponse.json({ + success: true, + image_base64: imageBase64, // 保留Base64用于前端立即显示 + thumbnail_url: thumbnailUrl, // S3缩略图URL + original_url: originalUrl, // S3原图URL + canvas_width: canvasWidth, + canvas_height: canvasHeight, + }); + } catch (error) { + console.error("[Resize API] 处理错误:", error); + return NextResponse.json( + { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" }, + { status: 500 } + ); + } +} + diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index e417aa2..cfc704b 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -2,8 +2,9 @@ import React, { useRef, useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; -import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand } from "lucide-react"; +import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2 } from "lucide-react"; import { Button } from "./ui/button"; +import { Input } from "./ui/input"; import { toast } from "sonner"; type Tool = "pen" | "arrow" | "circle" | "text"; @@ -64,8 +65,21 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator const [originalImageUrl, setOriginalImageUrl] = useState(imageUrl); const [processedImageUrl, setProcessedImageUrl] = useState(null); const [processedImageBase64, setProcessedImageBase64] = useState(null); - const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | null>(null); + const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | "resize" | null>(null); const [mounted, setMounted] = useState(false); + + // 尺寸调整相关状态 + const [isResizeMode, setIsResizeMode] = useState(false); + const [resizeCanvasWidth, setResizeCanvasWidth] = useState(1920); + const [resizeCanvasHeight, setResizeCanvasHeight] = useState(1080); + const [resizeBackgroundColor, setResizeBackgroundColor] = useState("#FFFFFF"); + const [resizeBackgroundTransparent, setResizeBackgroundTransparent] = useState(false); + const [resizeImageScale, setResizeImageScale] = useState(1); + const [resizeImagePosition, setResizeImagePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + const [isDraggingResize, setIsDraggingResize] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); + const resizeCanvasRef = useRef(null); + const resizeContainerRef = useRef(null); // 确保在客户端渲染 useEffect(() => { @@ -361,10 +375,15 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator const result = await response.json(); if (result.success && result.image_base64) { - // 保存处理后的图片URL和Base64 + // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { + console.log("[抠图] 原图URL:", result.original_url); setProcessedImageUrl(result.original_url); + } else if (result.thumbnail_url) { + console.warn("[抠图] ⚠️ 未找到original_url,使用thumbnail_url作为备选"); + setProcessedImageUrl(result.thumbnail_url); } else { + console.warn("[抠图] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } setProcessedImageBase64(result.image_base64); @@ -415,10 +434,15 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator const result = await response.json(); if (result.success && result.image_base64) { - // 保存处理后的图片URL和Base64 + // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { + console.log("[变清晰] 原图URL:", result.original_url); setProcessedImageUrl(result.original_url); + } else if (result.thumbnail_url) { + console.warn("[变清晰] ⚠️ 未找到original_url,使用thumbnail_url作为备选"); + setProcessedImageUrl(result.thumbnail_url); } else { + console.warn("[变清晰] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } setProcessedImageBase64(result.image_base64); @@ -436,6 +460,233 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator } }; + // 尺寸调整功能 + const handleResize = () => { + if (!image) return; + setIsResizeMode(true); + // 初始化尺寸为原图尺寸 + setResizeCanvasWidth(image.width); + setResizeCanvasHeight(image.height); + // 初始化图片位置为画布中心 + setResizeImagePosition({ x: 0, y: 0 }); + setResizeImageScale(1); + }; + + // 尺寸调整:处理图片拖拽 + const handleResizeImageDragStart = (e: React.MouseEvent | React.TouchEvent) => { + if (!isResizeMode || !resizeCanvasRef.current) return; + e.stopPropagation(); + setIsDraggingResize(true); + const canvas = resizeCanvasRef.current; + const rect = canvas.getBoundingClientRect(); + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; + const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; + + // 计算相对于画布的坐标 + const canvasX = ((clientX - rect.left) / rect.width) * canvas.width; + const canvasY = ((clientY - rect.top) / rect.height) * canvas.height; + + // 计算画布中心到图片中心的偏移(考虑显示缩放) + const containerWidth = resizeContainerRef.current?.clientWidth || canvas.width; + const containerHeight = resizeContainerRef.current?.clientHeight || canvas.height; + const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; + const containerAspect = containerWidth / containerHeight; + const displayScale = canvasAspect > containerAspect + ? (containerWidth / resizeCanvasWidth) + : (containerHeight / resizeCanvasHeight); + + // 保存拖拽起始点(相对于画布中心) + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const offsetX = (canvasX - centerX) / displayScale - resizeImagePosition.x; + const offsetY = (canvasY - centerY) / displayScale - resizeImagePosition.y; + setDragStart({ x: offsetX, y: offsetY }); + }; + + const handleResizeImageDrag = (e: React.MouseEvent | React.TouchEvent) => { + if (!isDraggingResize || !dragStart || !resizeCanvasRef.current) return; + e.preventDefault(); + e.stopPropagation(); + const canvas = resizeCanvasRef.current; + const rect = canvas.getBoundingClientRect(); + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; + const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; + + // 计算相对于画布的坐标 + const canvasX = ((clientX - rect.left) / rect.width) * canvas.width; + const canvasY = ((clientY - rect.top) / rect.height) * canvas.height; + + // 计算显示缩放 + const containerWidth = resizeContainerRef.current?.clientWidth || canvas.width; + const containerHeight = resizeContainerRef.current?.clientHeight || canvas.height; + const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; + const containerAspect = containerWidth / containerHeight; + const displayScale = canvasAspect > containerAspect + ? (containerWidth / resizeCanvasWidth) + : (containerHeight / resizeCanvasHeight); + + // 计算新的图片位置(相对于画布中心) + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const newX = ((canvasX - centerX) / displayScale) - dragStart.x; + const newY = ((canvasY - centerY) / displayScale) - dragStart.y; + + setResizeImagePosition({ x: newX, y: newY }); + }; + + const handleResizeImageDragEnd = () => { + setIsDraggingResize(false); + setDragStart(null); + }; + + // 尺寸调整:处理图片缩放(鼠标滚轮) + const handleResizeImageWheel = (e: React.WheelEvent) => { + if (!isResizeMode || !image) return; + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + const newScale = Math.max(0.1, Math.min(5, resizeImageScale + delta)); + setResizeImageScale(newScale); + }; + + // 尺寸调整:渲染预览 + useEffect(() => { + if (!isResizeMode || !resizeCanvasRef.current || !image || !resizeContainerRef.current) return; + + const canvas = resizeCanvasRef.current; + const container = resizeContainerRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + // 计算画布显示尺寸(适应容器) + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; + const containerAspect = containerWidth / containerHeight; + + let displayWidth: number; + let displayHeight: number; + let displayScale: number; + + if (canvasAspect > containerAspect) { + displayWidth = containerWidth; + displayHeight = containerWidth / canvasAspect; + displayScale = displayWidth / resizeCanvasWidth; + } else { + displayHeight = containerHeight; + displayWidth = containerHeight * canvasAspect; + displayScale = displayHeight / resizeCanvasHeight; + } + + canvas.width = displayWidth; + canvas.height = displayHeight; + + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 绘制背景(如果设置了背景颜色且不是透明) + if (!resizeBackgroundTransparent) { + ctx.fillStyle = resizeBackgroundColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + // 绘制图片(考虑缩放和位置) + const imageDisplayWidth = image.width * resizeImageScale * displayScale; + const imageDisplayHeight = image.height * resizeImageScale * displayScale; + const imageX = (canvas.width / 2) + (resizeImagePosition.x * displayScale); + const imageY = (canvas.height / 2) + (resizeImagePosition.y * displayScale); + + // 绘制图片(不设置透明度,保持原图清晰度) + ctx.drawImage( + image, + imageX - imageDisplayWidth / 2, + imageY - imageDisplayHeight / 2, + imageDisplayWidth, + imageDisplayHeight + ); + }, [isResizeMode, resizeCanvasWidth, resizeCanvasHeight, resizeBackgroundColor, resizeBackgroundTransparent, resizeImageScale, resizeImagePosition, image]); + + // 尺寸调整:导出为指定尺寸的图片并上传到存储桶 + const handleResizeConfirm = async () => { + if (!image || !isResizeMode) return; + + try { + // 创建指定尺寸的画布 + const exportCanvas = document.createElement("canvas"); + exportCanvas.width = resizeCanvasWidth; + exportCanvas.height = resizeCanvasHeight; + const exportCtx = exportCanvas.getContext("2d"); + if (!exportCtx) return; + + // 绘制背景 + if (!resizeBackgroundTransparent) { + exportCtx.fillStyle = resizeBackgroundColor; + exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); + } + + // 计算图片在画布上的实际尺寸和位置 + const imageWidth = image.width * resizeImageScale; + const imageHeight = image.height * resizeImageScale; + const imageX = (exportCanvas.width / 2) + resizeImagePosition.x; + const imageY = (exportCanvas.height / 2) + resizeImagePosition.y; + + // 绘制图片 + exportCtx.drawImage( + image, + imageX - imageWidth / 2, + imageY - imageHeight / 2, + imageWidth, + imageHeight + ); + + // 导出为图片 + const mimeType = resizeBackgroundTransparent ? "image/png" : "image/jpeg"; + const dataUrl = exportCanvas.toDataURL(mimeType, 0.95); + const base64Data = dataUrl.split(",")[1]; + + // 调用API上传到存储桶并保存到数据库 + const response = await fetch("/api/resize", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + image_base64: base64Data, + prompt: `尺寸调整处理 - ${resizeCanvasWidth}x${resizeCanvasHeight}`, + canvasWidth: resizeCanvasWidth, + canvasHeight: resizeCanvasHeight, + mimeType: mimeType, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "上传失败"); + } + + const result = await response.json(); + + if (result.success) { + // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) + if (result.original_url) { + console.log("[尺寸调整] 原图URL:", result.original_url); + setProcessedImageUrl(result.original_url); + } else { + setProcessedImageUrl(null); + } + setProcessedImageBase64(base64Data); + setProcessType("resize"); + setIsResizeMode(false); + + toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`); + } else { + throw new Error(result.error || "尺寸调整处理失败"); + } + } catch (error) { + console.error("[尺寸调整] 错误:", error); + toast.error(error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"); + } + }; + // 图像拓展处理 const handleExtend = async () => { if (!image || !canvasRef.current) return; @@ -471,8 +722,13 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator if (result.success && result.base64) { // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { + console.log("[图像拓展] 原图URL:", result.original_url); setProcessedImageUrl(result.original_url); + } else if (result.thumbnail_url) { + console.warn("[图像拓展] ⚠️ 未找到original_url,使用thumbnail_url作为备选"); + setProcessedImageUrl(result.thumbnail_url); } else { + console.warn("[图像拓展] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } setProcessedImageBase64(result.base64); @@ -490,29 +746,83 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator } }; - // 下载处理后的图片(必须使用原图URL) + // 下载处理后的图片(必须使用原图URL或Base64) const handleDownloadProcessed = async () => { - if (!processedImageUrl) { - toast.error("原图地址不可用,无法下载"); - return; - } - try { // 确定文件类型和MIME类型 - const isPng = processType === "rmbg"; + const isPng = processType === "rmbg" || processType === "resize"; const fileExtension = isPng ? "png" : "jpg"; + const mimeType = isPng ? "image/png" : "image/jpeg"; const fileName = processType === "rmbg" ? `抠图-${Date.now()}.${fileExtension}` : processType === "enhance" ? `变清晰-${Date.now()}.${fileExtension}` + : processType === "resize" + ? `尺寸调整-${resizeCanvasWidth}x${resizeCanvasHeight}-${Date.now()}.${fileExtension}` : `图像拓展-${Date.now()}.${fileExtension}`; + // 如果尺寸调整使用Base64,直接下载(优先使用URL,如果没有URL则使用Base64) + if (processType === "resize") { + if (processedImageUrl) { + // 如果有URL,使用URL下载 + const response = await fetch(processedImageUrl); + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + toast.success("图片下载成功"); + return; + } else if (processedImageBase64) { + // 如果没有URL,使用Base64下载 + const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; + const link = document.createElement("a"); + link.href = dataUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success("图片下载成功"); + return; + } else { + toast.error("图片数据不可用,无法下载"); + return; + } + } + + // 其他情况使用URL下载 + if (!processedImageUrl) { + toast.error("原图地址不可用,无法下载"); + return; + } + + // 确保使用的是原图URL(检查URL中是否包含thumb标识) + let downloadUrl = processedImageUrl; + if (downloadUrl.includes("-thumb") || downloadUrl.includes("/thumb/") || downloadUrl.includes("thumbnail")) { + // 如果URL包含thumb标识,尝试转换为原图URL + downloadUrl = downloadUrl + .replace(/-thumb-/g, "-orig-") + .replace(/\/thumb\//g, "/orig/") + .replace(/thumbnail/g, "original"); + console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl); + } + + console.log("[下载] 下载URL:", downloadUrl); + // 从原图URL下载(原图已上传到S3) - const response = await fetch(processedImageUrl); + const response = await fetch(downloadUrl); if (!response.ok) { - throw new Error("下载原图失败"); + throw new Error(`下载原图失败: ${response.status} ${response.statusText}`); } const blob = await response.blob(); + console.log("[下载] 下载的图片大小:", blob.size, "bytes"); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; @@ -525,7 +835,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator toast.success("原图下载成功"); } catch (error) { console.error("[下载] 错误:", error); - toast.error("下载失败,请稍后重试"); + toast.error(`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`); } }; @@ -540,7 +850,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator if (processedImageBase64) { // 如果有处理后的图片Base64,转换为data URL - const mimeType = processType === "rmbg" ? "image/png" : "image/jpeg"; + const mimeType = processType === "rmbg" || processType === "resize" ? "image/png" : "image/jpeg"; const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; onConfirm(dataUrl); return; @@ -595,7 +905,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator variant="ghost" size="sm" onClick={handleRemoveBackground} - disabled={isRemovingBg || isEnhancing || isExtending || !image} + disabled={isRemovingBg || isEnhancing || isExtending || isResizeMode || !image} title="移除背景(抠图)" > @@ -605,7 +915,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator variant="ghost" size="sm" onClick={handleEnhance} - disabled={isEnhancing || isRemovingBg || isExtending || !image} + disabled={isEnhancing || isRemovingBg || isExtending || isResizeMode || !image} title="图片变清晰" > @@ -615,12 +925,22 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator variant="ghost" size="sm" onClick={handleExtend} - disabled={isExtending || isRemovingBg || isEnhancing || !image} + disabled={isExtending || isRemovingBg || isEnhancing || isResizeMode || !image} title="图像拓展" > {isExtending ? "处理中..." : "图像拓展"} + @@ -729,6 +1049,102 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator 正在加载图片... + ) : isResizeMode ? ( + /* 尺寸调整模式 */ +
+ {/* 尺寸调整控制面板 */} +
+
+ + setResizeCanvasWidth(Number.parseInt(e.target.value) || 1)} + className="w-24 h-8" + min={1} + max={10000} + /> + px +
+
+ + setResizeCanvasHeight(Number.parseInt(e.target.value) || 1)} + className="w-24 h-8" + min={1} + max={10000} + /> + px +
+
+ + setResizeBackgroundColor(e.target.value)} + disabled={resizeBackgroundTransparent} + className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50" + /> +
+
+ +
+
+ + +
+
+ + {/* 尺寸调整预览画布 */} +
+
+ +
+ 画布: {resizeCanvasWidth} x {resizeCanvasHeight} | 缩放: {(resizeImageScale * 100).toFixed(0)}% | 拖拽移动图片,滚轮缩放 +
+
+
+
) : ( <> {/* 原图画布 */} @@ -752,7 +1168,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
- {processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : "拓展结果"} + {processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}