优化尺寸调整功能:添加比例选择和修复原图显示

- 添加快捷比例选择功能(智能、21:9、16:9、3:2、4:3、1:1、3:4、2:3、9:16)
- 选择比例后,输入宽度时高度自动根据比例计算
- 优化左右对比布局,减少空白,画布显示更大
- 修复点击确认调整后原图消失的问题,保持原图一直显示
- 优化工具栏布局,尺寸调整模式下显示专用工具栏
This commit is contained in:
Your Name 2025-12-29 23:43:17 +08:00
parent aab3b1e8ed
commit a8e5408136

View File

@ -78,9 +78,68 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
const [resizeImagePosition, setResizeImagePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [resizeImagePosition, setResizeImagePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [isDraggingResize, setIsDraggingResize] = useState(false); const [isDraggingResize, setIsDraggingResize] = useState(false);
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null);
const [selectedAspectRatio, setSelectedAspectRatio] = useState<string | null>(null); // 选中的比例,null表示自定义
const resizeCanvasRef = useRef<HTMLCanvasElement>(null); const resizeCanvasRef = useRef<HTMLCanvasElement>(null);
const resizeContainerRef = useRef<HTMLDivElement>(null); const resizeContainerRef = useRef<HTMLDivElement>(null);
// 比例选项定义
const aspectRatios = [
{ label: "智能", value: "smart", ratio: null }, // 智能:根据原图比例
{ label: "21:9", value: "21:9", ratio: 21 / 9 },
{ label: "16:9", value: "16:9", ratio: 16 / 9 },
{ label: "3:2", value: "3:2", ratio: 3 / 2 },
{ label: "4:3", value: "4:3", ratio: 4 / 3 },
{ label: "1:1", value: "1:1", ratio: 1 / 1 },
{ label: "3:4", value: "3:4", ratio: 3 / 4 },
{ label: "2:3", value: "2:3", ratio: 2 / 3 },
{ label: "9:16", value: "9:16", ratio: 9 / 16 },
];
// 根据比例和宽度计算高度
const calculateHeightFromWidth = (width: number, ratio: number | null) => {
if (ratio === null) {
// 智能模式:使用原图比例
if (image) {
return Math.round(width * (image.height / image.width));
}
return resizeCanvasHeight; // 如果没有原图,保持当前高度
}
return Math.round(width / ratio);
};
// 处理宽度变化
const handleWidthChange = (newWidth: number) => {
setResizeCanvasWidth(newWidth);
if (selectedAspectRatio) {
const ratioConfig = aspectRatios.find((r) => r.value === selectedAspectRatio);
if (ratioConfig) {
const newHeight = calculateHeightFromWidth(newWidth, ratioConfig.ratio);
setResizeCanvasHeight(newHeight);
}
}
};
// 处理高度变化(如果用户手动修改高度,取消比例锁定)
const handleHeightChange = (newHeight: number) => {
setResizeCanvasHeight(newHeight);
// 如果用户手动修改高度,取消比例锁定
if (selectedAspectRatio) {
setSelectedAspectRatio(null);
}
};
// 处理比例选择
const handleAspectRatioSelect = (value: string | null) => {
setSelectedAspectRatio(value);
if (value) {
const ratioConfig = aspectRatios.find((r) => r.value === value);
if (ratioConfig) {
const newHeight = calculateHeightFromWidth(resizeCanvasWidth, ratioConfig.ratio);
setResizeCanvasHeight(newHeight);
}
}
};
// 确保在客户端渲染 // 确保在客户端渲染
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@ -467,6 +526,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
// 初始化尺寸为原图尺寸 // 初始化尺寸为原图尺寸
setResizeCanvasWidth(image.width); setResizeCanvasWidth(image.width);
setResizeCanvasHeight(image.height); setResizeCanvasHeight(image.height);
setSelectedAspectRatio(null); // 重置比例选择
// 初始化图片位置为画布中心 // 初始化图片位置为画布中心
setResizeImagePosition({ x: 0, y: 0 }); setResizeImagePosition({ x: 0, y: 0 });
setResizeImageScale(1); setResizeImageScale(1);
@ -687,7 +747,8 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
} }
setProcessedImageBase64(base64Data); setProcessedImageBase64(base64Data);
setProcessType("resize"); setProcessType("resize");
setIsResizeMode(false); // 不退出尺寸调整模式,保持原图显示
// setIsResizeMode(false);
toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`); toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`);
} else { } else {
@ -969,89 +1030,127 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
{/* 工具选择栏 / 尺寸调整工具栏 */} {/* 工具选择栏 / 尺寸调整工具栏 */}
{isResizeMode ? ( {isResizeMode ? (
/* 尺寸调整模式下显示尺寸调整工具栏 */ /* 尺寸调整模式下显示尺寸调整工具栏 */
<div className="flex items-center gap-4 border-b px-4 py-3 bg-muted/30"> <div className="flex flex-col gap-3 border-b px-4 py-3 bg-muted/30">
{/* 比例选择 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-sm font-medium">宽度:</label> <label className="text-sm font-medium whitespace-nowrap">选择比例:</label>
<Input <div className="flex items-center gap-2 overflow-x-auto pb-1">
type="number" {aspectRatios.map((ratio) => (
value={resizeCanvasWidth} <button
onChange={(e) => setResizeCanvasWidth(Number.parseInt(e.target.value) || 1)} key={ratio.value}
className="w-24 h-8" type="button"
min={1} onClick={() => handleAspectRatioSelect(ratio.value === selectedAspectRatio ? null : ratio.value)}
max={10000} className={`flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-lg border transition-colors whitespace-nowrap ${
/> selectedAspectRatio === ratio.value
<span className="text-xs text-muted-foreground">px</span> ? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-muted border-border"
}`}
title={ratio.label}
>
<div
className={`border-2 rounded flex items-center justify-center ${
selectedAspectRatio === ratio.value ? "border-primary-foreground" : "border-foreground"
}`}
style={{
width: ratio.value === "smart" ? "16px" : ratio.ratio && ratio.ratio > 1 ? "24px" : ratio.ratio && ratio.ratio < 1 ? "12px" : "16px",
height: ratio.value === "smart" ? "16px" : ratio.ratio && ratio.ratio > 1 ? "12px" : ratio.ratio && ratio.ratio < 1 ? "24px" : "16px",
}}
>
{ratio.value === "smart" && (
<Maximize2 className="h-3 w-3" />
)}
</div>
<span className="text-xs">{ratio.label}</span>
</button>
))}
</div>
</div> </div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium">高度:</label> {/* 尺寸输入 */}
<Input <div className="flex items-center gap-4">
type="number" <div className="flex items-center gap-2">
value={resizeCanvasHeight} <label className="text-sm font-medium">宽度:</label>
onChange={(e) => setResizeCanvasHeight(Number.parseInt(e.target.value) || 1)} <Input
className="w-24 h-8" type="number"
min={1} value={resizeCanvasWidth}
max={10000} onChange={(e) => handleWidthChange(Number.parseInt(e.target.value) || 1)}
/> className="w-24 h-8"
<span className="text-xs text-muted-foreground">px</span> min={1}
</div> max={10000}
<div className="flex items-center gap-2">
<label className="text-sm font-medium">背景:</label>
<input
type="color"
value={resizeBackgroundColor}
onChange={(e) => setResizeBackgroundColor(e.target.value)}
disabled={resizeBackgroundTransparent}
className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium flex items-center gap-1">
<input
type="checkbox"
checked={resizeBackgroundTransparent}
onChange={(e) => setResizeBackgroundTransparent(e.target.checked)}
className="w-4 h-4"
/> />
透明 <span className="text-xs text-muted-foreground">px</span>
</label> </div>
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2 ml-auto"> <label className="text-sm font-medium">高度:</label>
<Button <Input
variant="outline" type="number"
size="sm" value={resizeCanvasHeight}
onClick={handleResizeZoomOut} onChange={(e) => handleHeightChange(Number.parseInt(e.target.value) || 1)}
title="缩小" className="w-24 h-8"
className="h-8 w-8 p-0" min={1}
> max={10000}
<ZoomOut className="h-4 w-4" /> />
</Button> <span className="text-xs text-muted-foreground">px</span>
<Button </div>
variant="outline" <div className="flex items-center gap-2">
size="sm" <label className="text-sm font-medium">背景:</label>
onClick={handleResizeZoomIn} <input
title="放大" type="color"
className="h-8 w-8 p-0" value={resizeBackgroundColor}
> onChange={(e) => setResizeBackgroundColor(e.target.value)}
<ZoomIn className="h-4 w-4" /> disabled={resizeBackgroundTransparent}
</Button> className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50"
<Button />
variant="ghost" </div>
size="sm" <div className="flex items-center gap-2">
onClick={() => { <label className="text-sm font-medium flex items-center gap-1">
setIsResizeMode(false); <input
setResizeImageScale(1); type="checkbox"
setResizeImagePosition({ x: 0, y: 0 }); checked={resizeBackgroundTransparent}
}} onChange={(e) => setResizeBackgroundTransparent(e.target.checked)}
> className="w-4 h-4"
取消 />
</Button> 透明
<Button </label>
size="sm" </div>
onClick={handleResizeConfirm} <div className="flex items-center gap-2 ml-auto">
className="bg-primary text-primary-foreground hover:bg-primary/90" <Button
> variant="outline"
确认调整 size="sm"
</Button> onClick={handleResizeZoomOut}
title="缩小"
className="h-8 w-8 p-0"
>
<ZoomOut className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleResizeZoomIn}
title="放大"
className="h-8 w-8 p-0"
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setIsResizeMode(false);
setResizeImageScale(1);
setResizeImagePosition({ x: 0, y: 0 });
}}
>
取消
</Button>
<Button
size="sm"
onClick={handleResizeConfirm}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
确认调整
</Button>
</div>
</div> </div>
</div> </div>
) : ( ) : (
@ -1125,7 +1224,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
)} )}
{/* 画布区域 - 并排显示原图和处理后的图片 */} {/* 画布区域 - 并排显示原图和处理后的图片 */}
<div ref={containerRef} className="flex flex-1 items-center justify-center gap-4 overflow-hidden bg-muted/30 p-4"> <div ref={containerRef} className="flex flex-1 items-center justify-center gap-2 overflow-hidden bg-muted/30 p-2">
{isLoading ? ( {isLoading ? (
/* 加载动画 */ /* 加载动画 */
<div className="flex flex-col items-center justify-center gap-3"> <div className="flex flex-col items-center justify-center gap-3">
@ -1149,26 +1248,32 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
/* 尺寸调整模式 - 左右对比布局 */ /* 尺寸调整模式 - 左右对比布局 */
<> <>
{/* 左侧:原图 */} {/* 左侧:原图 */}
<div className="flex flex-1 flex-col border-2 border-dashed border-border rounded-lg bg-muted/20 p-4 overflow-hidden"> <div className="flex flex-1 flex-col border-2 border-dashed border-border rounded-lg bg-muted/20 p-2 overflow-hidden">
<div className="mb-2 text-sm font-medium text-muted-foreground text-center">原图</div> <div className="mb-1 text-xs font-medium text-muted-foreground text-center">原图</div>
<div className="flex-1 flex items-center justify-center overflow-auto"> <div className="flex-1 flex items-center justify-center overflow-auto min-h-0">
{image && ( {image && (
<img <img
src={image.src} src={image.src}
alt="原图" alt="原图"
className="max-w-full max-h-full rounded-lg shadow-lg" className="max-w-full max-h-full rounded-lg shadow-lg"
style={{ objectFit: "contain" }} style={{
objectFit: "contain",
maxWidth: "100%",
maxHeight: "100%",
width: "auto",
height: "auto"
}}
/> />
)} )}
</div> </div>
</div> </div>
{/* 右侧:尺寸调整预览画布 */} {/* 右侧:尺寸调整预览画布 */}
<div className="flex flex-1 flex-col border-2 border-dashed border-primary/50 rounded-lg bg-muted/30 p-4 overflow-hidden"> <div className="flex flex-1 flex-col border-2 border-dashed border-primary/50 rounded-lg bg-muted/30 p-2 overflow-hidden">
<div className="mb-2 text-sm font-medium text-muted-foreground text-center">调整尺寸</div> <div className="mb-1 text-xs font-medium text-muted-foreground text-center">调整尺寸</div>
<div <div
ref={resizeContainerRef} ref={resizeContainerRef}
className="flex-1 flex items-center justify-center overflow-auto" className="flex-1 flex items-center justify-center overflow-auto min-h-0"
onWheel={handleResizeImageWheel} onWheel={handleResizeImageWheel}
> >
<div className="relative inline-block"> <div className="relative inline-block">