From 1a216501a91a3e19422a257dbf4e4b3493b5f103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 27 Jul 2026 16:56:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=9F=E5=AD=90=E7=BB=B4=E6=8A=A4=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E9=80=89=E6=8B=A9=E4=B8=8E=E9=94=9A=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将选中素材集合和范围锚点合并到同一个状态 让单击、全选、可见协调、框选和模式切换返回纯状态转移 新增可见范围协调后 Shift 选择的回归测试 --- .../useImageCanvasAssetLibrary.test.tsx | 36 +++++++++ .../useImageCanvasAssetLibrary.ts | 75 ++++++++++--------- 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/src/components/image-editor/useImageCanvasAssetLibrary.test.tsx b/src/components/image-editor/useImageCanvasAssetLibrary.test.tsx index 3642649b6..a02d1c826 100644 --- a/src/components/image-editor/useImageCanvasAssetLibrary.test.tsx +++ b/src/components/image-editor/useImageCanvasAssetLibrary.test.tsx @@ -215,6 +215,14 @@ function AssetLibraryHarness({ > toggle all + @@ -518,6 +526,34 @@ describe('useImageCanvasAssetLibrary', () => { expect(screen.getByTestId('selected-assets').textContent).toBe('asset-c'); }); + it('reconciles selected ids and their range anchor atomically', async () => { + loadEditorAssetLibraryMock.mockResolvedValueOnce({ + folders: [ + { + folderId: 'project', + label: '项目素材', + sortOrder: 0, + collapsed: false, + systemDefault: true, + }, + ], + assets: [ + createAssetSnapshot({ assetId: 'asset-a', label: '素材A' }), + createAssetSnapshot({ assetId: 'asset-b', label: '素材B' }), + createAssetSnapshot({ assetId: 'asset-c', label: '素材C' }), + ], + }); + render(); + + fireEvent.click(await screen.findByRole('button', { name: '素材A' })); + fireEvent.click(screen.getByRole('button', { name: 'reconcile to b c' })); + fireEvent.click(screen.getByRole('button', { name: '素材C' }), { + shiftKey: true, + }); + + expect(screen.getByTestId('selected-assets').textContent).toBe('asset-c'); + }); + it('submits a generated asset to showcase review', async () => { loadEditorAssetLibraryMock.mockResolvedValueOnce({ folders: [ diff --git a/src/components/image-editor/useImageCanvasAssetLibrary.ts b/src/components/image-editor/useImageCanvasAssetLibrary.ts index 63f02a52c..3cd870bb7 100644 --- a/src/components/image-editor/useImageCanvasAssetLibrary.ts +++ b/src/components/image-editor/useImageCanvasAssetLibrary.ts @@ -22,6 +22,7 @@ import { } from '../../services/image-editor/editorProjectClient'; import { type AssetSelectionInteraction, + type AssetSelectionState, createAssetMarqueeFromPointer, createAssetMarqueeSelectionRect, createLocalAssetFolder, @@ -76,6 +77,13 @@ function formatShowcaseSubmitError(error: unknown) { return '提交精选审核失败,请稍后重试'; } +function createEmptyAssetSelection(): AssetSelectionState { + return { + selectedAssetIds: new Set(), + anchorAssetId: null, + }; +} + const ASSET_FOLDER_SELECTOR = '[data-asset-folder-id]'; const ASSET_ITEM_SELECTOR = '[data-asset-id]'; const ASSET_MARQUEE_BLOCKED_TARGET_SELECTOR = @@ -140,10 +148,10 @@ export function useImageCanvasAssetLibrary({ const [newFolderName, setNewFolderName] = useState(''); const [activeUploadFolderId, setActiveUploadFolderId] = useState('project'); const [isAssetSelectionMode, setIsAssetSelectionMode] = useState(false); - const [selectedAssetIds, setSelectedAssetIds] = useState>( - () => new Set(), + const [assetSelection, setAssetSelection] = useState( + createEmptyAssetSelection, ); - const assetSelectionAnchorRef = useRef(null); + const { selectedAssetIds } = assetSelection; const marqueeBaseSelectionRef = useRef>(new Set()); const [assetMarquee, setAssetMarquee] = useState( null, @@ -485,40 +493,34 @@ export function useImageCanvasAssetLibrary({ const toggleAssetSelected = useCallback( (assetId: string, interaction: AssetSelectionInteraction) => { - setSelectedAssetIds((currentIds) => { - const nextSelection = resolveAssetSelection({ - selectedAssetIds: currentIds, - anchorAssetId: assetSelectionAnchorRef.current, + setAssetSelection((currentSelection) => + resolveAssetSelection({ + ...currentSelection, targetAssetId: assetId, ...interaction, - }); - assetSelectionAnchorRef.current = nextSelection.anchorAssetId; - return nextSelection.selectedAssetIds; - }); + }), + ); }, [], ); const toggleAllAssetsSelected = useCallback((visibleAssetIds: string[]) => { - assetSelectionAnchorRef.current = null; - setSelectedAssetIds((currentIds) => - resolveVisibleAssetSelectionToggle({ - selectedAssetIds: currentIds, + setAssetSelection((currentSelection) => ({ + selectedAssetIds: resolveVisibleAssetSelectionToggle({ + selectedAssetIds: currentSelection.selectedAssetIds, visibleAssetIds, }), - ); + anchorAssetId: null, + })); }, []); const reconcileSelectedAssets = useCallback((visibleAssetIds: string[]) => { - setSelectedAssetIds((currentIds) => { - const nextSelection = reconcileAssetSelection({ - selectedAssetIds: currentIds, - anchorAssetId: assetSelectionAnchorRef.current, + setAssetSelection((currentSelection) => + reconcileAssetSelection({ + ...currentSelection, visibleAssetIds, - }); - assetSelectionAnchorRef.current = nextSelection.anchorAssetId; - return nextSelection.selectedAssetIds; - }); + }), + ); }, []); const deleteSelectedAssets = useCallback(() => { @@ -532,8 +534,7 @@ export function useImageCanvasAssetLibrary({ removeSelectedAssets(currentAssets, selectedAssetIds).assets, ); onDeleteAssets?.(deletedAssets); - setSelectedAssetIds(new Set()); - assetSelectionAnchorRef.current = null; + setAssetSelection(createEmptyAssetSelection()); ids.forEach((assetId) => { void deleteEditorAsset(assetId); }); @@ -557,18 +558,16 @@ export function useImageCanvasAssetLibrary({ const closeAssetSelectionMode = useCallback(() => { setIsAssetSelectionMode(false); - setSelectedAssetIds(new Set()); - assetSelectionAnchorRef.current = null; + setAssetSelection(createEmptyAssetSelection()); marqueeBaseSelectionRef.current = new Set(); setAssetMarquee(null); }, []); const openAssetSelectionMode = useCallback(() => { - assetSelectionAnchorRef.current = null; marqueeBaseSelectionRef.current = new Set(); setRenamingAsset(null); setRenamingFolder(null); - setSelectedAssetIds(new Set()); + setAssetSelection(createEmptyAssetSelection()); setAssetMarquee(null); setIsAssetSelectionMode(true); }, []); @@ -585,9 +584,13 @@ export function useImageCanvasAssetLibrary({ assetTargets: readAssetHitTargets(assetListRef.current), selectionRect, }); - setSelectedAssetIds( - new Set([...marqueeBaseSelectionRef.current, ...marqueeSelection]), - ); + setAssetSelection({ + selectedAssetIds: new Set([ + ...marqueeBaseSelectionRef.current, + ...marqueeSelection, + ]), + anchorAssetId: null, + }); }, [assetListRef, assets], ); @@ -617,8 +620,10 @@ export function useImageCanvasAssetLibrary({ ); marqueeBaseSelectionRef.current = event.ctrlKey || event.metaKey ? new Set(selectedAssetIds) : new Set(); - assetSelectionAnchorRef.current = null; - setSelectedAssetIds(new Set(marqueeBaseSelectionRef.current)); + setAssetSelection({ + selectedAssetIds: new Set(marqueeBaseSelectionRef.current), + anchorAssetId: null, + }); }, [assetListRef, isAssetSelectionMode, selectedAssetIds], );