refactor: align canvas image toolbar with available features
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-02-20 23:15:10 +08:00
parent 435fbd10f7
commit c80b46d43c

View File

@ -2,7 +2,7 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react";
import { ImagePlus, Loader2, AlertCircle, MoreVertical, MoreHorizontal, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@ -1617,10 +1617,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Move3D className="h-4 w-4 mr-2" />
角度编辑
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('scene-fusion')}>
<Layers className="h-4 w-4 mr-2" />
详情替换
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('prompt-regenerate')}>
<RefreshCw className="h-4 w-4 mr-2" />
提示词重绘
@ -1732,7 +1728,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{isSelected && !isMultiSelected && data.src && !isEditingTitle && typeof document !== 'undefined' && createPortal(
<TooltipProvider delayDuration={200}>
<div
className="fixed flex items-center gap-0.5 rounded-full shadow-xl px-2 py-1 z-[9999] bg-white/95 border border-gray-200 text-gray-800 dark:bg-gray-900/95 dark:border-gray-700 dark:text-gray-100"
className="fixed z-[9999] flex items-center gap-1 rounded-[22px] border border-slate-300 bg-slate-50/95 px-3 py-2 text-gray-800 shadow-[0_8px_24px_rgba(15,23,42,0.14)] backdrop-blur-sm dark:border-slate-600 dark:bg-slate-900/95 dark:text-gray-100"
style={{
top: getToolbarPosition().top,
left: getToolbarPosition().left,
@ -1743,226 +1739,127 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{/* 添加到对话 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing || isAddingToChat}
className="h-8 w-8 rounded-full hover:bg-blue-50 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 dark:hover:bg-blue-900/30 disabled:opacity-50 disabled:pointer-events-none"
onClick={handleAddToChat}
>
{isAddingToChat ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MessageSquarePlus className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">添加到对话</TooltipContent>
</Tooltip>
{/* 🆕 物品拾取 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-orange-50 text-orange-600 hover:text-orange-700 dark:text-orange-400 dark:hover:text-orange-300 dark:hover:bg-orange-900/30 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => {
setPickerMode(true, node.id);
toast.info('点击图片中的物品进行拾取');
}}
>
<Crosshair className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">拾取物品</TooltipContent>
</Tooltip>
{/* 分隔线 */}
<div className="h-5 w-px bg-gray-200 dark:bg-gray-700 mx-0.5" />
{/* 放大/高清 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('enhance')}
>
<span className="text-[10px] font-medium text-gray-500 border border-gray-300 rounded px-1 dark:text-gray-200 dark:border-gray-600">HD</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">高清放大</TooltipContent>
</Tooltip>
{/* 移除背景/抠图 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('rmbg')}
>
<Scissors className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">移除背景</TooltipContent>
</Tooltip>
<Button
variant="ghost"
disabled={isProcessing || isAddingToChat}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={handleAddToChat}
>
{isAddingToChat ? (
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
) : (
<MessageSquarePlus className="mr-1.5 h-4 w-4 text-blue-600" />
)}
添加到对话
</Button>
{/* 去水印 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('watermark')}
>
<Eraser className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">去水印</TooltipContent>
</Tooltip>
{/* 文字编辑 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing || isDetectingText || isApplyingTextEdit}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('text-edit')}
>
<Type className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">编辑文字</TooltipContent>
</Tooltip>
<div className="mx-1 h-5 w-px bg-slate-200 dark:bg-slate-700" />
{/* 图片备注 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className={cn(
"h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none",
data.note && "text-amber-600 dark:text-amber-300"
)}
onClick={() => handleAction('note')}
>
<StickyNote className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">图片备注</TooltipContent>
</Tooltip>
<Button
variant="ghost"
disabled={isProcessing}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={() => handleAction('enhance')}
>
<span className="mr-1.5 rounded-md border border-slate-300 px-1.5 text-[11px] leading-5 text-slate-600 dark:border-slate-500 dark:text-slate-100">HD</span>
放大
</Button>
{/* 提示词重绘 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
disabled={isProcessing}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={() => handleAction('rmbg')}
>
<Scissors className="mr-1.5 h-4 w-4" />
移除背景
</Button>
<Button
variant="ghost"
disabled={isProcessing}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={() => handleAction('watermark')}
>
<Eraser className="mr-1.5 h-4 w-4" />
擦除
</Button>
<Button
variant="ghost"
disabled={isProcessing}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={() => {
setPickerMode(true, node.id);
toast.info('点击图片中的物品进行拾取');
}}
>
<Crosshair className="mr-1.5 h-4 w-4 text-orange-600" />
拾取物品
</Button>
<Button
variant="ghost"
disabled={isProcessing || isDetectingText || isApplyingTextEdit}
className="h-9 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={() => handleAction('text-edit')}
>
<Type className="mr-1.5 h-4 w-4" />
编辑文字
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('prompt-regenerate')}
className="h-9 w-9 rounded-xl text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
>
<RefreshCw className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">提示词重绘</TooltipContent>
</Tooltip>
{/* 角度编辑 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('angle')}
>
<Move3D className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">角度编辑</TooltipContent>
</Tooltip>
{/* 扩展 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('extend')}
>
<Expand className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">图片扩展</TooltipContent>
</Tooltip>
{/* 尺寸调整 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={() => handleAction('resize')}
>
<Maximize2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">尺寸调整</TooltipContent>
</Tooltip>
{/* 下载 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none"
onClick={handleDownload}
>
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">下载图片</TooltipContent>
</Tooltip>
{/* 删除 */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-8 w-8 rounded-full hover:bg-red-50 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900/30 disabled:opacity-50 disabled:pointer-events-none"
</DropdownMenuTrigger>
<DropdownMenuContent align="center" side="bottom">
<DropdownMenuItem onClick={() => handleAction('note')}>
<StickyNote className="mr-2 h-4 w-4" />
图片备注
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('prompt-regenerate')}>
<RefreshCw className="mr-2 h-4 w-4" />
提示词重绘
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('angle')}>
<Move3D className="mr-2 h-4 w-4" />
角度编辑
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('extend')}>
<Expand className="mr-2 h-4 w-4" />
图片扩展
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAction('resize')}>
<Maximize2 className="mr-2 h-4 w-4" />
尺寸调整
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600 focus:text-red-700"
onClick={() => deleteNode(node.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">删除</TooltipContent>
</Tooltip>
<Trash2 className="mr-2 h-4 w-4" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-9 w-9 rounded-xl text-slate-700 hover:bg-white hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
onClick={handleDownload}
>
<Download className="h-4 w-4" />
</Button>
</div>
</TooltipProvider>,
document.body