抽取图片画布通用组件

新增图片画布图标按钮和侧栏媒体项 primitives

让素材文件夹按钮和生成关闭按钮复用通用图标按钮

移除主画布视图内重复的按钮与列表项实现
This commit is contained in:
2026-06-13 22:23:19 +08:00
parent 242860e2d3
commit b2122481ff
2 changed files with 114 additions and 118 deletions

View File

@@ -0,0 +1,96 @@
import type { ComponentType, ReactNode } from 'react';
type IconComponent = ComponentType<{ className?: string }>;
export type EditorIconButtonProps = {
label: string;
title?: string;
icon: IconComponent;
className?: string;
type?: 'button' | 'submit';
disabled?: boolean;
pressed?: boolean;
expanded?: boolean;
onClick?: () => void;
};
export function EditorIconButton({
label,
title = label,
icon: Icon,
className,
type = 'button',
disabled,
pressed,
expanded,
onClick,
}: EditorIconButtonProps) {
return (
<button
type={type}
className={className}
aria-label={label}
title={title}
disabled={disabled}
aria-pressed={pressed}
aria-expanded={expanded}
onClick={onClick}
>
<Icon className="h-4 w-4" />
</button>
);
}
export type SidebarMediaItemProps = {
title: string;
detail: string;
imageSrc: string;
imageAlt: string;
selected?: boolean;
primaryLabel: string;
onPrimaryClick: () => void;
thumbnailClassName: string;
metaClassName: string;
rowClassName: string;
primaryClassName?: string;
actions?: ReactNode;
titleNode?: ReactNode;
};
export function SidebarMediaItem({
title,
detail,
imageSrc,
imageAlt,
selected = false,
primaryLabel,
onPrimaryClick,
thumbnailClassName,
metaClassName,
rowClassName,
primaryClassName,
actions,
titleNode,
}: SidebarMediaItemProps) {
return (
<div
className={`${rowClassName} ${selected ? `${rowClassName}--selected` : ''}`}
>
<button
type="button"
className={primaryClassName}
onClick={onPrimaryClick}
aria-label={primaryLabel}
>
<span className={thumbnailClassName}>
<img src={imageSrc} alt={imageAlt} />
</span>
</button>
<div className={metaClassName}>
{titleNode ?? <span>{title}</span>}
<span>{detail}</span>
</div>
{actions}
</div>
);
}

View File

@@ -26,10 +26,8 @@ import {
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { import {
type ComponentType,
type KeyboardEvent as ReactKeyboardEvent, type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -48,6 +46,10 @@ import {
saveEditorProjectLayout, saveEditorProjectLayout,
} from '../../services/image-editor/editorProjectClient'; } from '../../services/image-editor/editorProjectClient';
import { ApiClientError } from '../../services/apiClient'; import { ApiClientError } from '../../services/apiClient';
import {
EditorIconButton,
SidebarMediaItem,
} from './ImageCanvasEditorPrimitives';
type EditorAsset = { type EditorAsset = {
id: string; id: string;
@@ -141,34 +143,6 @@ type SnapCandidate = {
distance: number; distance: number;
}; };
type EditorIconButtonProps = {
label: string;
title?: string;
icon: ComponentType<{ className?: string }>;
className?: string;
type?: 'button' | 'submit';
disabled?: boolean;
pressed?: boolean;
expanded?: boolean;
onClick?: () => void;
};
type SidebarMediaItemProps = {
title: string;
detail: string;
imageSrc: string;
imageAlt: string;
selected?: boolean;
primaryLabel: string;
onPrimaryClick: () => void;
thumbnailClassName: string;
metaClassName: string;
rowClassName: string;
primaryClassName?: string;
actions?: ReactNode;
titleNode?: ReactNode;
};
type DragState = type DragState =
| { | {
kind: 'pan'; kind: 'pan';
@@ -304,71 +278,6 @@ const CANVAS_BACKGROUND_OPTIONS = [
{ label: '冷蓝', value: '#eef6ff' }, { label: '冷蓝', value: '#eef6ff' },
]; ];
function EditorIconButton({
label,
title = label,
icon: Icon,
className,
type = 'button',
disabled,
pressed,
expanded,
onClick,
}: EditorIconButtonProps) {
return (
<button
type={type}
className={className}
aria-label={label}
title={title}
disabled={disabled}
aria-pressed={pressed}
aria-expanded={expanded}
onClick={onClick}
>
<Icon className="h-4 w-4" />
</button>
);
}
function SidebarMediaItem({
title,
detail,
imageSrc,
imageAlt,
selected = false,
primaryLabel,
onPrimaryClick,
thumbnailClassName,
metaClassName,
rowClassName,
primaryClassName,
actions,
titleNode,
}: SidebarMediaItemProps) {
return (
<div
className={`${rowClassName} ${selected ? `${rowClassName}--selected` : ''}`}
>
<button
type="button"
className={primaryClassName}
onClick={onPrimaryClick}
aria-label={primaryLabel}
>
<span className={thumbnailClassName}>
<img src={imageSrc} alt={imageAlt} />
</span>
</button>
<div className={metaClassName}>
{titleNode ?? <span>{title}</span>}
<span>{detail}</span>
</div>
{actions}
</div>
);
}
function clamp(value: number, min: number, max: number) { function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }
@@ -1604,32 +1513,25 @@ export function ImageCanvasEditorView() {
aria-label={folder.label} aria-label={folder.label}
> >
<div className="image-canvas-editor__asset-folder-header"> <div className="image-canvas-editor__asset-folder-header">
<button <EditorIconButton
type="button" label={`${folder.collapsed ? '展开' : '折叠'}${folder.label}`}
aria-label={`${folder.collapsed ? '展开' : '折叠'}${folder.label}`} title={folder.collapsed ? '展开' : '折叠'}
aria-expanded={!folder.collapsed} icon={folder.collapsed ? ChevronRight : ChevronDown}
expanded={!folder.collapsed}
onClick={() => toggleAssetFolder(folder.id)} onClick={() => toggleAssetFolder(folder.id)}
> />
{folder.collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
<Folder className="h-4 w-4" /> <Folder className="h-4 w-4" />
<span>{folder.label}</span> <span>{folder.label}</span>
<span>{folder.assets.length}</span> <span>{folder.assets.length}</span>
<button <EditorIconButton
type="button" label={`上传到${folder.label}`}
aria-label={`上传到${folder.label}`}
title="上传" title="上传"
icon={ImagePlus}
onClick={() => { onClick={() => {
setActiveUploadFolderId(folder.id); setActiveUploadFolderId(folder.id);
uploadInputRef.current?.click(); uploadInputRef.current?.click();
}} }}
> />
<ImagePlus className="h-4 w-4" />
</button>
</div> </div>
<div <div
className="image-canvas-editor__asset-folder-list" className="image-canvas-editor__asset-folder-list"
@@ -2192,18 +2094,16 @@ export function ImageCanvasEditorView() {
{generateDialog.errorMessage} {generateDialog.errorMessage}
</div> </div>
) : null} ) : null}
<button <EditorIconButton
type="button"
className="image-canvas-editor__generation-close" className="image-canvas-editor__generation-close"
aria-label="关闭生成图片" label="关闭生成图片"
icon={X}
disabled={generateDialog.status === 'generating'} disabled={generateDialog.status === 'generating'}
onClick={() => { onClick={() => {
setGenerateDialog(null); setGenerateDialog(null);
setActiveTool('select'); setActiveTool('select');
}} }}
> />
<X className="h-4 w-4" />
</button>
</form> </form>
) : null} ) : null}
</div> </div>