切换图片画布抠图到 BiRefNet
新增图片画布去背景 BFF 配置与路由,服务端代理 BiRefNet 并持久化结果。
将手动去背景、角色图、图标 spritesheet、UI 素材提取和角色动作帧抠图切到 BiRefNet。
新增画布右上角任务侧栏并展示去背景任务耗时与状态。
同步图片画布技术文档和项目决策记录。
(cherry picked from commit 495fcc8801)
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
ListChecks,
|
||||
Loader2,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasTaskListItem,
|
||||
CanvasTaskStatus,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
type TaskSidebarItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
detail: string;
|
||||
status: CanvasTaskStatus;
|
||||
elapsedMs?: number;
|
||||
errorMessage?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type ImageCanvasTaskSidebarViewProps = {
|
||||
generationDialogs: CanvasGenerationDialogState[];
|
||||
backgroundRemovalTasks: CanvasTaskListItem[];
|
||||
open: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
};
|
||||
|
||||
function formatElapsedMs(elapsedMs?: number) {
|
||||
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs)) {
|
||||
return null;
|
||||
}
|
||||
const seconds = elapsedMs / 1000;
|
||||
return seconds >= 10 ? `${Math.round(seconds)}秒` : `${seconds.toFixed(1)}秒`;
|
||||
}
|
||||
|
||||
function generationDialogTitle(dialog: CanvasGenerationDialogState) {
|
||||
switch (dialog.mode) {
|
||||
case 'quick-edit':
|
||||
return '局部修改';
|
||||
case 'character-animation':
|
||||
return '角色动画';
|
||||
case 'video':
|
||||
return '视频生成';
|
||||
case 'audio-sound-effect':
|
||||
return '音效生成';
|
||||
case 'audio-background-music':
|
||||
return '背景音乐';
|
||||
case 'icon':
|
||||
return '图标素材';
|
||||
case 'ui-design':
|
||||
return 'UI素材提取';
|
||||
case 'publication':
|
||||
return '宣发素材';
|
||||
case 'character':
|
||||
return '角色图片';
|
||||
case 'spec':
|
||||
return '图片规范';
|
||||
default:
|
||||
return '图片生成';
|
||||
}
|
||||
}
|
||||
|
||||
function generationDialogDetail(dialog: CanvasGenerationDialogState) {
|
||||
if (dialog.status === 'generating') {
|
||||
return '生成中';
|
||||
}
|
||||
if (dialog.status === 'failed') {
|
||||
return dialog.errorMessage || '生成失败';
|
||||
}
|
||||
return dialog.generatedLayerId ? '生成完成' : '待提交';
|
||||
}
|
||||
|
||||
function statusLabel(status: CanvasTaskStatus) {
|
||||
if (status === 'pending') {
|
||||
return '待提交';
|
||||
}
|
||||
if (status === 'running') {
|
||||
return '进行中';
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return '失败';
|
||||
}
|
||||
return '完成';
|
||||
}
|
||||
|
||||
function statusIcon(status: CanvasTaskStatus) {
|
||||
if (status === 'pending') {
|
||||
return <Clock3 className="h-3.5 w-3.5" aria-hidden="true" />;
|
||||
}
|
||||
if (status === 'running') {
|
||||
return <Loader2 className="h-3.5 w-3.5" aria-hidden="true" />;
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return <XCircle className="h-3.5 w-3.5" aria-hidden="true" />;
|
||||
}
|
||||
return <CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
export function ImageCanvasTaskSidebarView({
|
||||
generationDialogs,
|
||||
backgroundRemovalTasks,
|
||||
open,
|
||||
onToggleOpen,
|
||||
onActivateGenerationDialog,
|
||||
}: ImageCanvasTaskSidebarViewProps) {
|
||||
const items = useMemo<TaskSidebarItem[]>(() => {
|
||||
const generationItems: TaskSidebarItem[] = generationDialogs.map(
|
||||
(dialog) => ({
|
||||
id: `generation-${dialog.id}`,
|
||||
title: generationDialogTitle(dialog),
|
||||
detail: generationDialogDetail(dialog),
|
||||
status:
|
||||
dialog.status === 'generating'
|
||||
? 'running'
|
||||
: dialog.status === 'failed'
|
||||
? 'failed'
|
||||
: dialog.generatedLayerId
|
||||
? 'done'
|
||||
: 'pending',
|
||||
errorMessage: dialog.errorMessage,
|
||||
onClick: () => onActivateGenerationDialog(dialog),
|
||||
}),
|
||||
);
|
||||
return [...backgroundRemovalTasks, ...generationItems];
|
||||
}, [backgroundRemovalTasks, generationDialogs, onActivateGenerationDialog]);
|
||||
|
||||
if (!open && items.length === 0) {
|
||||
return (
|
||||
<div className="image-canvas-editor__task-sidebar image-canvas-editor__task-sidebar--closed">
|
||||
<EditorIconButton
|
||||
className="image-canvas-editor__task-sidebar-toggle"
|
||||
label="任务列表"
|
||||
title="任务列表"
|
||||
icon={ListChecks}
|
||||
expanded={false}
|
||||
onClick={onToggleOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`image-canvas-editor__task-sidebar ${open ? 'image-canvas-editor__task-sidebar--open' : 'image-canvas-editor__task-sidebar--closed'}`}
|
||||
aria-label="画布任务列表"
|
||||
>
|
||||
<EditorIconButton
|
||||
className="image-canvas-editor__task-sidebar-toggle"
|
||||
label="任务列表"
|
||||
title="任务列表"
|
||||
icon={ListChecks}
|
||||
expanded={open}
|
||||
onClick={onToggleOpen}
|
||||
/>
|
||||
{open ? (
|
||||
<div className="image-canvas-editor__task-sidebar-panel">
|
||||
<div className="image-canvas-editor__task-sidebar-header">
|
||||
<strong>任务列表</strong>
|
||||
<span>{items.length}</span>
|
||||
</div>
|
||||
{items.length > 0 ? (
|
||||
<div className="image-canvas-editor__task-sidebar-list">
|
||||
{items.map((item) => {
|
||||
const elapsedLabel = formatElapsedMs(item.elapsedMs);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`image-canvas-editor__task-sidebar-item image-canvas-editor__task-sidebar-item--${item.status}`}
|
||||
onClick={item.onClick}
|
||||
disabled={!item.onClick}
|
||||
>
|
||||
<span className="image-canvas-editor__task-sidebar-item-icon">
|
||||
{statusIcon(item.status)}
|
||||
</span>
|
||||
<span className="image-canvas-editor__task-sidebar-item-main">
|
||||
<span className="image-canvas-editor__task-sidebar-item-title">
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="image-canvas-editor__task-sidebar-item-detail">
|
||||
{item.detail}
|
||||
{elapsedLabel ? ` · 耗时 ${elapsedLabel}` : ''}
|
||||
{item.errorMessage && item.detail !== item.errorMessage
|
||||
? ` · ${item.errorMessage}`
|
||||
: ''}
|
||||
</span>
|
||||
</span>
|
||||
<span className="image-canvas-editor__task-sidebar-item-status">
|
||||
{statusLabel(item.status)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user