抽取图片画布通用组件
新增图片画布图标按钮和侧栏媒体项 primitives 让素材文件夹按钮和生成关闭按钮复用通用图标按钮 移除主画布视图内重复的按钮与列表项实现
This commit is contained in:
96
src/components/image-editor/ImageCanvasEditorPrimitives.tsx
Normal file
96
src/components/image-editor/ImageCanvasEditorPrimitives.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user