优化尺寸调整功能:添加比例选择和修复原图显示
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

- 添加快捷比例选择功能(智能、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 [isDraggingResize, setIsDraggingResize] = useState(false);
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null);
const [selectedAspectRatio, setSelectedAspectRatio] = useState<string | null>(null); // 选中的比例,null表示自定义
const resizeCanvasRef = useRef<HTMLCanvasElement>(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(() => {
setMounted(true);
@ -467,6 +526,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
// 初始化尺寸为原图尺寸
setResizeCanvasWidth(image.width);
setResizeCanvasHeight(image.height);
setSelectedAspectRatio(null); // 重置比例选择
// 初始化图片位置为画布中心
setResizeImagePosition({ x: 0, y: 0 });
setResizeImageScale(1);
@ -687,7 +747,8 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
}
setProcessedImageBase64(base64Data);
setProcessType("resize");
setIsResizeMode(false);
// 不退出尺寸调整模式,保持原图显示
// setIsResizeMode(false);
toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`);
} else {
@ -969,89 +1030,127 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
{/* 工具选择栏 / 尺寸调整工具栏 */}
{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">
<label className="text-sm font-medium">宽度:</label>
<Input
type="number"
value={resizeCanvasWidth}
onChange={(e) => setResizeCanvasWidth(Number.parseInt(e.target.value) || 1)}
className="w-24 h-8"
min={1}
max={10000}
/>
<span className="text-xs text-muted-foreground">px</span>
<label className="text-sm font-medium whitespace-nowrap">选择比例:</label>
<div className="flex items-center gap-2 overflow-x-auto pb-1">
{aspectRatios.map((ratio) => (
<button
key={ratio.value}
type="button"
onClick={() => handleAspectRatioSelect(ratio.value === selectedAspectRatio ? null : ratio.value)}
className={`flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-lg border transition-colors whitespace-nowrap ${
selectedAspectRatio === ratio.value
? "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 className="flex items-center gap-2">
<label className="text-sm font-medium">高度:</label>
<Input
type="number"
value={resizeCanvasHeight}
onChange={(e) => setResizeCanvasHeight(Number.parseInt(e.target.value) || 1)}
className="w-24 h-8"
min={1}
max={10000}
/>
<span className="text-xs text-muted-foreground">px</span>
</div>
<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"
{/* 尺寸输入 */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">宽度:</label>
<Input
type="number"
value={resizeCanvasWidth}
onChange={(e) => handleWidthChange(Number.parseInt(e.target.value) || 1)}
className="w-24 h-8"
min={1}
max={10000}
/>
透明
</label>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button
variant="outline"
size="sm"
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>
<span className="text-xs text-muted-foreground">px</span>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium">高度:</label>
<Input
type="number"
value={resizeCanvasHeight}
onChange={(e) => handleHeightChange(Number.parseInt(e.target.value) || 1)}
className="w-24 h-8"
min={1}
max={10000}
/>
<span className="text-xs text-muted-foreground">px</span>
</div>
<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"
/>
透明
</label>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button
variant="outline"
size="sm"
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>
) : (
@ -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 ? (
/* 加载动画 */
<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="mb-2 text-sm font-medium text-muted-foreground text-center">原图</div>
<div className="flex-1 flex items-center justify-center overflow-auto">
<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-1 text-xs font-medium text-muted-foreground text-center">原图</div>
<div className="flex-1 flex items-center justify-center overflow-auto min-h-0">
{image && (
<img
src={image.src}
alt="原图"
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 className="flex flex-1 flex-col border-2 border-dashed border-primary/50 rounded-lg bg-muted/30 p-4 overflow-hidden">
<div className="mb-2 text-sm font-medium text-muted-foreground text-center">调整尺寸</div>
<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-1 text-xs font-medium text-muted-foreground text-center">调整尺寸</div>
<div
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}
>
<div className="relative inline-block">