Files
Genarrative/src/components/image-editor/ImageCanvasSidebarView.tsx
T
kdletters 636934de88 补齐陶泥儿精选审核链路
新增 editor_showcase_asset、点赞和活动配置表,生成素材默认不公开。

补齐素材提交精选审核、后台审核通过返还泥点、展示开关和公开精选分页接口。

更新素材库右键菜单和创作主页精选瀑布流,展示作者昵称、陶泥号、成本和点赞状态。

新增后台素材查询页,支持时间、用户、分类、关键词查询和缩略图详情查看。

同步 SpacetimeDB 绑定、前后端契约、项目文档和决策记录。
2026-07-04 15:47:14 +08:00

343 lines
12 KiB
TypeScript

import {
CheckSquare,
FolderPlus,
Square,
} from 'lucide-react';
import type {
Dispatch,
PointerEvent as ReactPointerEvent,
RefObject,
SetStateAction,
} from 'react';
import { useMemo, useState } from 'react';
import { PlatformTextField } from '../common/PlatformTextField';
import {
ImageCanvasAssetLibraryPanelView,
type GroupedEditorAssetFolder,
type UploadFilesOptions,
type UploadRequestOptions,
} from './ImageCanvasAssetLibraryPanelView';
import {
filterGroupedAssetsBySearch,
layerMatchesSearch,
layerStartsWithSearch,
} from './ImageCanvasAssetLibraryModel';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type {
AssetMarqueeState,
AssetPointerDragState,
CanvasContextMenuState,
CanvasLayer,
EditorAsset,
EditorAssetFolder,
ImageContextMenuState,
SidebarPanel,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasLayerPanelView } from './ImageCanvasLayerPanelView';
export type { GroupedEditorAssetFolder, UploadFilesOptions };
export type ImageCanvasSidebarViewProps = {
activeSidebarPanel: SidebarPanel | null;
assetListRef: RefObject<HTMLDivElement | null>;
assetPointerDragRef: { current: AssetPointerDragState | null };
suppressAssetClickRef: { current: boolean };
assets: EditorAsset[];
groupedAssets: GroupedEditorAssetFolder[];
assetFolders: EditorAssetFolder[];
layers: CanvasLayer[];
selectedLayerId: string | null;
selectedLayerIds: string[];
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;
setIsAssetSelectionMode: Dispatch<SetStateAction<boolean>>;
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>>>;
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
setContextMenu: Dispatch<SetStateAction<CanvasContextMenuState | null>>;
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, options?: UploadRequestOptions) => 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;
submitAssetToShowcase: (asset: EditorAsset) => void;
toggleAssetSelected: (assetId: string) => void;
addAssetLayer: (asset: EditorAsset) => void;
onDownloadAsset: (asset: EditorAsset) => void;
toggleAllAssetsSelected: () => void;
deleteSelectedAssets: () => void;
closeAssetSelectionMode: () => void;
groupSelectedLayers: () => void;
selectSingleLayer: (layerId: string | null) => void;
resolveContextMenuPosition: (
clientX: number,
clientY: number,
menuKind: 'blank' | 'layer',
) => Omit<CanvasContextMenuState, 'kind' | 'layerId' | 'canvasPoint'>;
getCanvasPointFromClient: (
clientX: number,
clientY: number,
) => { x: number; y: number };
};
export function ImageCanvasSidebarView({
activeSidebarPanel,
assetListRef,
assetPointerDragRef,
suppressAssetClickRef,
groupedAssets,
assetFolders,
layers,
selectedLayerId,
selectedLayerIds,
isAssetSelectionMode,
selectedAssetIds,
assetMoveDropFolderId,
pinnedAssetMoveFolderId,
creatingFolder,
newFolderName,
renamingFolder,
renamingAsset,
allSelectableAssetsSelected,
assetMarquee,
setIsAssetSelectionMode,
setCreatingFolder,
setNewFolderName,
setRenamingFolder,
setRenamingAsset,
setActiveUploadFolderId,
setUploadDropTarget,
setAssetPointerDrag,
setSelectedAssetIds,
setImageContextMenu,
setContextMenu,
onAssetMarqueePointerDown,
onAssetMarqueePointerMove,
onAssetMarqueePointerUp,
updateAssetMoveDropFolder,
addUploadedFiles,
requestUpload,
moveAssetToFolder,
commitNewAssetFolder,
toggleAssetFolder,
startRenamingFolder,
commitFolderRename,
deleteAssetFolder,
startRenamingAsset,
commitAssetRename,
deleteUploadedAsset,
submitAssetToShowcase,
toggleAssetSelected,
addAssetLayer,
onDownloadAsset,
toggleAllAssetsSelected,
deleteSelectedAssets,
closeAssetSelectionMode,
groupSelectedLayers,
selectSingleLayer,
resolveContextMenuPosition,
getCanvasPointFromClient,
}: ImageCanvasSidebarViewProps) {
const [assetSearchQuery, setAssetSearchQuery] = useState('');
const [layerSearchQuery, setLayerSearchQuery] = useState('');
const filteredGroupedAssets = useMemo(
() => filterGroupedAssetsBySearch(groupedAssets, assetSearchQuery),
[assetSearchQuery, groupedAssets],
);
const filteredAssetsCount = useMemo(
() =>
filteredGroupedAssets.reduce(
(totalCount, folder) => totalCount + folder.assets.length,
0,
),
[filteredGroupedAssets],
);
const normalizedLayerSearchQuery = layerSearchQuery.trim().toLowerCase();
const filteredLayers = useMemo(
() =>
normalizedLayerSearchQuery
? [
...layers.filter((layer) =>
layerStartsWithSearch(layer, normalizedLayerSearchQuery),
),
...layers.filter(
(layer) =>
!layerStartsWithSearch(layer, normalizedLayerSearchQuery) &&
layerMatchesSearch(layer, normalizedLayerSearchQuery),
),
]
: layers,
[layers, normalizedLayerSearchQuery],
);
if (!activeSidebarPanel) {
return null;
}
return (
<aside className="image-canvas-editor__sidebar" aria-label="图片资源栏">
<div className="image-canvas-editor__sidebar-header">
<div className="min-w-0">
<h2 className="image-canvas-editor__sidebar-title">
{activeSidebarPanel === 'assets' ? '素材' : '图层'}
</h2>
<div className="image-canvas-editor__sidebar-count">
{activeSidebarPanel === 'assets'
? filteredAssetsCount
: filteredLayers.length}
</div>
</div>
{activeSidebarPanel === 'assets' ? (
<div className="image-canvas-editor__sidebar-header-actions">
<EditorIconButton
className="image-canvas-editor__icon-button"
label="素材选择模式"
title="选择"
icon={isAssetSelectionMode ? CheckSquare : Square}
pressed={isAssetSelectionMode}
onClick={() =>
setIsAssetSelectionMode((currentMode) => !currentMode)
}
/>
<EditorIconButton
className="image-canvas-editor__icon-button"
label="新建素材文件夹"
title="新建文件夹"
icon={FolderPlus}
onClick={() => setCreatingFolder(true)}
/>
</div>
) : (
<EditorIconButton
className="image-canvas-editor__icon-button"
label="图层打组"
title="打组"
icon={FolderPlus}
disabled={!selectedLayerId && selectedLayerIds.length === 0}
onClick={groupSelectedLayers}
/>
)}
</div>
<div className="image-canvas-editor__sidebar-search">
<PlatformTextField
aria-label={
activeSidebarPanel === 'assets' ? '搜索素材' : '搜索画布图层'
}
placeholder={activeSidebarPanel === 'assets' ? '搜索素材' : '搜索图层'}
value={
activeSidebarPanel === 'assets' ? assetSearchQuery : layerSearchQuery
}
size="xs"
density="compact"
className="image-canvas-editor__sidebar-search-input"
onChange={(event) => {
if (activeSidebarPanel === 'assets') {
setAssetSearchQuery(event.target.value);
} else {
setLayerSearchQuery(event.target.value);
}
}}
/>
</div>
{activeSidebarPanel === 'assets' ? (
<ImageCanvasAssetLibraryPanelView
assetListRef={assetListRef}
assetPointerDragRef={assetPointerDragRef}
suppressAssetClickRef={suppressAssetClickRef}
groupedAssets={filteredGroupedAssets}
assetFolders={assetFolders}
isAssetSelectionMode={isAssetSelectionMode}
selectedAssetIds={selectedAssetIds}
assetMoveDropFolderId={assetMoveDropFolderId}
pinnedAssetMoveFolderId={pinnedAssetMoveFolderId}
creatingFolder={creatingFolder}
newFolderName={newFolderName}
renamingFolder={renamingFolder}
renamingAsset={renamingAsset}
allSelectableAssetsSelected={allSelectableAssetsSelected}
assetMarquee={assetMarquee}
setCreatingFolder={setCreatingFolder}
setNewFolderName={setNewFolderName}
setRenamingFolder={setRenamingFolder}
setRenamingAsset={setRenamingAsset}
setActiveUploadFolderId={setActiveUploadFolderId}
setUploadDropTarget={setUploadDropTarget}
setAssetPointerDrag={setAssetPointerDrag}
setSelectedAssetIds={setSelectedAssetIds}
onAssetMarqueePointerDown={onAssetMarqueePointerDown}
onAssetMarqueePointerMove={onAssetMarqueePointerMove}
onAssetMarqueePointerUp={onAssetMarqueePointerUp}
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
addUploadedFiles={addUploadedFiles}
requestUpload={requestUpload}
moveAssetToFolder={moveAssetToFolder}
commitNewAssetFolder={commitNewAssetFolder}
toggleAssetFolder={toggleAssetFolder}
startRenamingFolder={startRenamingFolder}
commitFolderRename={commitFolderRename}
deleteAssetFolder={deleteAssetFolder}
startRenamingAsset={startRenamingAsset}
commitAssetRename={commitAssetRename}
deleteUploadedAsset={deleteUploadedAsset}
submitAssetToShowcase={submitAssetToShowcase}
toggleAssetSelected={toggleAssetSelected}
addAssetLayer={addAssetLayer}
onDownloadAsset={onDownloadAsset}
toggleAllAssetsSelected={toggleAllAssetsSelected}
deleteSelectedAssets={deleteSelectedAssets}
closeAssetSelectionMode={closeAssetSelectionMode}
/>
) : (
<ImageCanvasLayerPanelView
layers={filteredLayers}
selectedLayerId={selectedLayerId}
selectedLayerIds={selectedLayerIds}
setImageContextMenu={setImageContextMenu}
setContextMenu={setContextMenu}
selectSingleLayer={selectSingleLayer}
resolveContextMenuPosition={resolveContextMenuPosition}
getCanvasPointFromClient={getCanvasPointFromClient}
/>
)}
</aside>
);
}