fix: simplify image annotator toolbar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-06 19:19:54 +08:00
parent 593a00cebc
commit 3f1bc41ae9

View File

@ -4,7 +4,7 @@ import React, { useRef, useState, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import { useTheme } from "next-themes";
import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon, Droplet, Redo, MousePointer2 } from "lucide-react";
import { X, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon, Droplet, Redo, MousePointer2 } from "lucide-react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Slider } from "./ui/slider";
@ -56,20 +56,6 @@ interface ImageAnnotatorProps {
onOpenChange?: (open: boolean) => void;
}
const COLORS = [
"#FF0000", // 红色
"#FF6B00", // 橙色
"#FFCC00", // 黄色
"#00CC00", // 绿色
"#0099FF", // 蓝色
"#9933FF", // 紫色
"#FF0099", // 粉色
"#FFFFFF", // 白色
"#000000", // 黑色
];
const LINE_WIDTHS = [2, 4, 6, 8];
export function ImageAnnotator({
imageUrl,
onConfirm,
@ -85,9 +71,9 @@ export function ImageAnnotator({
const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [tool, setTool] = useState<Tool>("pen");
const [color, setColor] = useState("#FF0000");
const [lineWidth, setLineWidth] = useState(4);
const tool: Tool = "pen";
const color = "#FF0000";
const lineWidth = 4;
const [actions, setActions] = useState<DrawingAction[]>([]);
const [currentAction, setCurrentAction] = useState<DrawingAction | null>(null);
const [textInput, setTextInput] = useState("");
@ -1463,285 +1449,6 @@ export function ImageAnnotator({
</div>
)}
{/* Floating Toolbar - Top (Tools & Actions) */}
{!showSceneFusion && !isResizeMode && (
<div className="absolute left-1/2 top-[72px] z-20 -translate-x-1/2 pointer-events-none w-full max-w-[90vw] flex justify-center">
<div className="pointer-events-auto flex items-center gap-1 rounded-[30px] border border-black/10 bg-white/95 p-2 shadow-xl backdrop-blur-xl dark:bg-zinc-900/95 dark:border-white/10 overflow-x-auto max-w-full scrollbar-hide">
{!isResizeMode && (
<>
{/* Drawing Tools */}
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
tool === "pen" && "bg-black text-white hover:bg-black dark:bg-white dark:text-black dark:hover:bg-white"
)}
onClick={() => setTool("pen")}
>
<Pencil className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>画笔</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
tool === "arrow" && "bg-black text-white hover:bg-black dark:bg-white dark:text-black dark:hover:bg-white"
)}
onClick={() => setTool("arrow")}
>
<ArrowRight className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>箭头</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
tool === "circle" && "bg-black text-white hover:bg-black dark:bg-white dark:text-black dark:hover:bg-white"
)}
onClick={() => setTool("circle")}
>
<Circle className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>圆圈</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
tool === "text" && "bg-black text-white hover:bg-black dark:bg-white dark:text-black dark:hover:bg-white"
)}
onClick={() => setTool("text")}
>
<Type className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>文字</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="mx-1 h-7 w-px bg-black/10 dark:bg-white/10" />
{/* Colors */}
<div className="flex items-center gap-1 px-1">
{COLORS.slice(0, 5).map((c) => (
<button
key={c}
type="button"
className={cn(
"h-5 w-5 rounded-full border border-black/15 transition-all",
color === c && "scale-110 ring-2 ring-black/25 dark:ring-white/25"
)}
style={{ backgroundColor: c }}
onClick={() => setColor(c)}
title={c}
/>
))}
<input
type="color"
value={color}
onChange={(event) => setColor(event.target.value)}
className="h-5 w-5 cursor-pointer rounded-full border border-black/15 bg-transparent p-0"
title="自定义颜色"
/>
</div>
<div className="mx-1 h-7 w-px bg-black/10 dark:bg-white/10" />
{/* Line Width */}
<div className="flex items-center gap-1">
{LINE_WIDTHS.map((w) => (
<button
key={w}
type="button"
className={cn(
"flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800",
lineWidth === w && "bg-zinc-200 dark:bg-zinc-700"
)}
onClick={() => setLineWidth(w)}
title={`线宽 ${w}px`}
>
<div className="rounded-full bg-zinc-800 dark:bg-zinc-200" style={{ width: w + 2, height: w + 2 }} />
</button>
))}
</div>
<div className="mx-1 h-7 w-px bg-black/10 dark:bg-white/10" />
</>
)}
{/* AI Actions */}
<TooltipProvider delayDuration={0}>
{!isResizeMode && (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleRemoveBackground}
disabled={isRemovingBg}
>
<Scissors className={cn("h-5 w-5", isRemovingBg && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>移除背景</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleEnhance}
disabled={isEnhancing}
>
<Sparkles className={cn("h-5 w-5", isEnhancing && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>变清晰</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleExtend}
disabled={isExtending}
>
<Expand className={cn("h-5 w-5", isExtending && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>图像拓展</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleWatermarkRemove}
disabled={isRemovingWatermark}
>
<Droplet className={cn("h-5 w-5", isRemovingWatermark && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>去水印</TooltipContent>
</Tooltip>
<div className="mx-1 h-7 w-px bg-black/10 dark:bg-white/10" />
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleResize}
>
<Maximize2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>尺寸调整</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleAngleEdit}
>
<Move3D className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>角度调整</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={() => setShowSceneFusion(true)}
>
<Layers className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>批量处理</TooltipContent>
</Tooltip>
<div className="mx-1 h-7 w-px bg-black/10 dark:bg-white/10" />
</>
)}
{/* History */}
{!isResizeMode && (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleUndo}
disabled={actions.length === 0}
>
<Undo className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleClear}
disabled={actions.length === 0}
>
<Trash2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>清空</TooltipContent>
</Tooltip>
</>
)}
</TooltipProvider>
</div>
</div>
)}
{/* Main Content Area */}
<div className="relative flex h-full w-full bg-[#f4f4f5] dark:bg-[#09090b]">
{showSceneFusion ? (
@ -1758,7 +1465,7 @@ export function ImageAnnotator({
{/* Main Canvas / Image Area */}
<div className={cn(
"relative z-10 flex items-center justify-center w-full h-full p-8",
isResizeMode ? "pb-36" : "pb-24"
isResizeMode ? "pb-36" : "pb-32"
)}>
{isLoading ? (
<div className="flex flex-col items-center justify-center gap-3">
@ -2015,23 +1722,164 @@ export function ImageAnnotator({
)}
{!showSceneFusion && !isResizeMode && (
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20 pointer-events-none">
<div className="pointer-events-auto flex items-center gap-2 p-1.5 bg-background/95 backdrop-blur-xl border border-border/50 shadow-2xl rounded-[24px]">
<Button
variant="ghost"
onClick={onCancel}
className="rounded-xl px-4 text-muted-foreground hover:text-foreground"
>
取消
</Button>
<div className="h-4 w-px bg-border/50" />
<Button
onClick={handleConfirm}
className="rounded-xl px-6 bg-primary text-primary-foreground hover:bg-primary/90"
>
<Check className="mr-2 h-4 w-4" />
{confirmButtonText}
</Button>
<div className="absolute bottom-8 left-1/2 z-20 w-[min(94vw,1120px)] -translate-x-1/2 pointer-events-none">
<div className="pointer-events-auto flex items-center justify-between gap-3 overflow-x-auto rounded-[28px] border border-border/60 bg-background px-3 py-2 shadow-2xl scrollbar-hide">
<TooltipProvider delayDuration={0}>
<div className="flex min-w-max items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleRemoveBackground}
disabled={isRemovingBg}
>
<Scissors className={cn("h-5 w-5", isRemovingBg && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>移除背景</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleEnhance}
disabled={isEnhancing}
>
<Sparkles className={cn("h-5 w-5", isEnhancing && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>变清晰</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleExtend}
disabled={isExtending}
>
<Expand className={cn("h-5 w-5", isExtending && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>图像拓展</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleWatermarkRemove}
disabled={isRemovingWatermark}
>
<Droplet className={cn("h-5 w-5", isRemovingWatermark && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>去水印</TooltipContent>
</Tooltip>
<div className="mx-1 h-7 w-px shrink-0 bg-border/60" />
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleResize}
>
<Maximize2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>尺寸调整</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleAngleEdit}
>
<Move3D className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>角度调整</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={() => setShowSceneFusion(true)}
>
<Layers className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>批量处理</TooltipContent>
</Tooltip>
<div className="mx-1 h-7 w-px shrink-0 bg-border/60" />
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleUndo}
disabled={actions.length === 0}
>
<Undo className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-10 w-10 rounded-full text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
onClick={handleClear}
disabled={actions.length === 0}
>
<Trash2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>清空</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="ghost"
onClick={onCancel}
className="rounded-[18px] px-4 text-muted-foreground hover:text-foreground"
>
取消
</Button>
<Button
onClick={handleConfirm}
className="rounded-[18px] px-6 bg-primary text-primary-foreground hover:bg-primary/90"
>
<Check className="mr-2 h-4 w-4" />
{confirmButtonText}
</Button>
</div>
</div>
</div>
)}