Files
Genarrative/src/components/image-editor/ImageCanvasAssetLibraryPanelView.tsx
kdletters d8b935317d 拆分编辑器前端画布视图
抽出素材栏、生成器、舞台工具栏和画布世界视图

补充各拆分视图的聚焦测试

更新 TRACKING.md 记录第三十四阶段验证
2026-06-17 17:48:12 +08:00

280 lines
9.2 KiB
TypeScript

import {
Check,
CheckSquare,
Folder,
Square,
Trash2,
X,
} from 'lucide-react';
import type {
Dispatch,
PointerEvent as ReactPointerEvent,
RefObject,
SetStateAction,
} from 'react';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar';
import { PlatformTextField } from '../common/PlatformTextField';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import { ImageCanvasAssetFolderSectionView } from './ImageCanvasAssetFolderSectionView';
import type {
AssetMarqueeState,
AssetPointerDragState,
EditorAsset,
EditorAssetFolder,
UploadTarget,
} from './ImageCanvasEditorTypes';
export type GroupedEditorAssetFolder = EditorAssetFolder & {
assets: EditorAsset[];
};
export type UploadFilesOptions = {
folderId?: string;
canvasPoint?: { x: number; y: number };
addToCanvas?: boolean;
};
export type ImageCanvasAssetLibraryPanelViewProps = {
assetListRef: RefObject<HTMLDivElement | null>;
assetPointerDragRef: { current: AssetPointerDragState | null };
suppressAssetClickRef: { current: boolean };
groupedAssets: GroupedEditorAssetFolder[];
assetFolders: EditorAssetFolder[];
isAssetSelectionMode: boolean;
selectedAssetIds: Set<string>;
assetMoveDropFolderId: string | null;
pinnedAssetMoveFolderId: string | null;
creatingFolder: boolean;
newFolderName: string;
renamingFolder: { folderId: string; value: string } | null;
renamingAsset: { assetId: string; value: string } | null;
allSelectableAssetsSelected: boolean;
assetMarquee: AssetMarqueeState | null;
setCreatingFolder: Dispatch<SetStateAction<boolean>>;
setNewFolderName: Dispatch<SetStateAction<string>>;
setRenamingFolder: Dispatch<
SetStateAction<{ folderId: string; value: string } | null>
>;
setRenamingAsset: Dispatch<
SetStateAction<{ assetId: string; value: string } | null>
>;
setActiveUploadFolderId: Dispatch<SetStateAction<string>>;
setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>;
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
onAssetMarqueePointerDown: (
event: ReactPointerEvent<HTMLDivElement>,
) => void;
onAssetMarqueePointerMove: (
event: ReactPointerEvent<HTMLDivElement>,
) => void;
onAssetMarqueePointerUp: (
event: ReactPointerEvent<HTMLDivElement>,
) => void;
updateAssetMoveDropFolder: (folderId: string | null) => void;
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
requestUpload: (target: UploadTarget) => void;
moveAssetToFolder: (assetId: string, folderId: string) => void;
commitNewAssetFolder: () => void | Promise<void>;
toggleAssetFolder: (folderId: string) => void;
startRenamingFolder: (folder: EditorAssetFolder) => void;
commitFolderRename: (folder: EditorAssetFolder) => void;
deleteAssetFolder: (folder: EditorAssetFolder) => void;
startRenamingAsset: (asset: EditorAsset) => void;
commitAssetRename: (asset: EditorAsset) => void;
deleteUploadedAsset: (asset: EditorAsset) => void;
toggleAssetSelected: (assetId: string) => void;
addAssetLayer: (asset: EditorAsset) => void;
toggleAllAssetsSelected: () => void;
deleteSelectedAssets: () => void;
closeAssetSelectionMode: () => void;
};
export function ImageCanvasAssetLibraryPanelView({
assetListRef,
assetPointerDragRef,
suppressAssetClickRef,
groupedAssets,
assetFolders,
isAssetSelectionMode,
selectedAssetIds,
assetMoveDropFolderId,
pinnedAssetMoveFolderId,
creatingFolder,
newFolderName,
renamingFolder,
renamingAsset,
allSelectableAssetsSelected,
assetMarquee,
setCreatingFolder,
setNewFolderName,
setRenamingFolder,
setRenamingAsset,
setActiveUploadFolderId,
setUploadDropTarget,
setAssetPointerDrag,
setSelectedAssetIds,
onAssetMarqueePointerDown,
onAssetMarqueePointerMove,
onAssetMarqueePointerUp,
updateAssetMoveDropFolder,
addUploadedFiles,
requestUpload,
moveAssetToFolder,
commitNewAssetFolder,
toggleAssetFolder,
startRenamingFolder,
commitFolderRename,
deleteAssetFolder,
startRenamingAsset,
commitAssetRename,
deleteUploadedAsset,
toggleAssetSelected,
addAssetLayer,
toggleAllAssetsSelected,
deleteSelectedAssets,
closeAssetSelectionMode,
}: ImageCanvasAssetLibraryPanelViewProps) {
return (
<div
ref={assetListRef}
className="image-canvas-editor__asset-list"
onPointerDown={onAssetMarqueePointerDown}
onPointerMove={onAssetMarqueePointerMove}
onPointerUp={onAssetMarqueePointerUp}
onPointerCancel={onAssetMarqueePointerUp}
>
{pinnedAssetMoveFolderId ? (
<div
className="image-canvas-editor__asset-folder-sticky-target"
aria-hidden="true"
>
<Folder className="h-4 w-4" />
<span>
{assetFolders.find((folder) => folder.id === pinnedAssetMoveFolderId)
?.label ?? '目标文件夹'}
</span>
</div>
) : null}
{creatingFolder ? (
<form
className="image-canvas-editor__folder-create"
onSubmit={(event) => {
event.preventDefault();
void commitNewAssetFolder();
}}
>
<PlatformTextField
aria-label="素材文件夹名称"
value={newFolderName}
autoFocus
size="xs"
density="compact"
className="image-canvas-editor__folder-create-input"
onChange={(event) => setNewFolderName(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
setCreatingFolder(false);
setNewFolderName('');
}
}}
/>
<EditorIconButton type="submit" label="保存素材文件夹" icon={Check} />
<EditorIconButton
label="取消新建素材文件夹"
icon={X}
onClick={() => {
setCreatingFolder(false);
setNewFolderName('');
}}
/>
</form>
) : null}
{groupedAssets.map((folder) => (
<ImageCanvasAssetFolderSectionView
key={folder.id}
folder={folder}
assetPointerDragRef={assetPointerDragRef}
suppressAssetClickRef={suppressAssetClickRef}
isAssetSelectionMode={isAssetSelectionMode}
selectedAssetIds={selectedAssetIds}
assetMoveDropFolderId={assetMoveDropFolderId}
renamingFolder={renamingFolder}
renamingAsset={renamingAsset}
setRenamingFolder={setRenamingFolder}
setRenamingAsset={setRenamingAsset}
setActiveUploadFolderId={setActiveUploadFolderId}
setUploadDropTarget={setUploadDropTarget}
setAssetPointerDrag={setAssetPointerDrag}
setSelectedAssetIds={setSelectedAssetIds}
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
addUploadedFiles={addUploadedFiles}
requestUpload={requestUpload}
moveAssetToFolder={moveAssetToFolder}
toggleAssetFolder={toggleAssetFolder}
startRenamingFolder={startRenamingFolder}
commitFolderRename={commitFolderRename}
deleteAssetFolder={deleteAssetFolder}
startRenamingAsset={startRenamingAsset}
commitAssetRename={commitAssetRename}
deleteUploadedAsset={deleteUploadedAsset}
toggleAssetSelected={toggleAssetSelected}
addAssetLayer={addAssetLayer}
/>
))}
{isAssetSelectionMode ? (
<PlatformBatchActionToolbar
className="image-canvas-editor__asset-batch-toolbar"
label="素材批量操作"
>
<PlatformActionButton
tone="secondary"
size="sm"
onClick={toggleAllAssetsSelected}
>
{allSelectableAssetsSelected ? (
<CheckSquare className="h-4 w-4" />
) : (
<Square className="h-4 w-4" />
)}
{selectedAssetIds.size > 0
? `${allSelectableAssetsSelected ? '取消全选' : '全选'} · 已选 ${selectedAssetIds.size}`
: '全选'}
</PlatformActionButton>
<PlatformActionButton
tone="warning"
size="sm"
disabled={selectedAssetIds.size === 0}
onClick={deleteSelectedAssets}
>
<Trash2 className="h-4 w-4" />
</PlatformActionButton>
<PlatformActionButton
tone="secondary"
size="sm"
onClick={closeAssetSelectionMode}
>
</PlatformActionButton>
</PlatformBatchActionToolbar>
) : null}
{assetMarquee ? (
<div
className="image-canvas-editor__asset-marquee"
aria-hidden="true"
style={{
left: Math.min(assetMarquee.startX, assetMarquee.currentX),
top: Math.min(assetMarquee.startY, assetMarquee.currentY),
width: Math.abs(assetMarquee.currentX - assetMarquee.startX),
height: Math.abs(assetMarquee.currentY - assetMarquee.startY),
}}
/>
) : null}
</div>
);
}