优化尺寸调整功能:添加比例选择和修复原图显示
- 添加快捷比例选择功能(智能、21:9、16:9、3:2、4:3、1:1、3:4、2:3、9:16) - 选择比例后,输入宽度时高度自动根据比例计算 - 优化左右对比布局,减少空白,画布显示更大 - 修复点击确认调整后原图消失的问题,保持原图一直显示 - 优化工具栏布局,尺寸调整模式下显示专用工具栏
This commit is contained in:
parent
aab3b1e8ed
commit
a8e5408136
@ -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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user