From ecac0dc3fcb29c06f4db837673a97e7eef1e828f Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 5 Jul 2026 10:45:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E6=9D=BF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E4=B8=8E=E5=A4=B1=E6=95=88=E9=A1=B9=E7=9B=AE=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 画板参考图选择提示改为持续显示并支持手动关闭。 显式项目访问失效时同步切回项目页状态。 补充提示关闭和项目失效回退测试。 --- .../image-editor/ImageCanvasEditorView.tsx | 9 +++++- .../useImageCanvasGenerationSurface.test.tsx | 2 ++ .../useImageCanvasGenerationSurface.tsx | 11 ++++++- .../useImageCanvasGenerationWorkflow.test.tsx | 24 ++++++++++++++ .../useImageCanvasGenerationWorkflow.ts | 11 ------- .../useImageCanvasProjectPersistence.test.tsx | 28 +++++++++++++++++ .../useImageCanvasProjectPersistence.ts | 31 ++++++++++++------- .../PlatformEntryFlowShellImpl.tsx | 10 +++++- src/index.css | 19 ++++++++++++ 9 files changed, 120 insertions(+), 25 deletions(-) diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index aea5cd610..901c4f807 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -110,6 +110,10 @@ const CANVAS_STARTUP_TOOLS: CanvasStartupTool[] = [ 'publication-cover', ]; +type ImageCanvasEditorViewProps = { + onProjectAccessLost?: () => void; +}; + function isCanvasStartupTool(value: string | null): value is CanvasStartupTool { return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool); } @@ -278,7 +282,9 @@ function createAssetActionLayer(asset: EditorAsset): CanvasLayer { }; } -export function ImageCanvasEditorView() { +export function ImageCanvasEditorView({ + onProjectAccessLost, +}: ImageCanvasEditorViewProps = {}) { const authUi = useAuthUi(); const [, setGenerationPricingVersion] = useState(0); const [walletBalanceLabel, setWalletBalanceLabel] = useState( @@ -996,6 +1002,7 @@ export function ImageCanvasEditorView() { canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, currentUserId: currentEditorUserId, openEditorLoginModal, + onProjectAccessLost, }); const applyGeneratedProjectSnapshot = useCallback( (project: EditorProjectSnapshot) => { diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx index f81996560..37776cff1 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx @@ -311,6 +311,8 @@ describe('useImageCanvasGenerationSurface', () => { expect(screen.getByRole('alert').textContent).toBe( '选择的图片不是角色规范图,请选择生成规范里的角色规范。', ); + fireEvent.click(screen.getByRole('button', { name: '关闭提示' })); + expect(screen.queryByRole('alert')).toBeNull(); }); it('anchors the music menu to the music tool and closes after hover leaves', () => { diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.tsx index a429a0ee2..8b2bafd85 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.tsx @@ -11,6 +11,7 @@ import { useRef, } from 'react'; import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; import { PlatformFloatingMenu, @@ -481,7 +482,15 @@ export function useImageCanvasGenerationSurface({ className="image-canvas-editor__reference-pick-toast" role="alert" > - {generationWorkflow.referencePickWarning} + {generationWorkflow.referencePickWarning} + ) : null} { expect(screen.getByTestId('reference-pick-warning').textContent).toBe('-'); }); + it('keeps reference pick warnings visible until the user takes a follow-up action', () => { + vi.useFakeTimers(); + try { + render(); + + fireEvent.click(screen.getByRole('button', { name: '打开角色生成' })); + fireEvent.click(screen.getByRole('button', { name: '选择非规范角色图' })); + + expect(screen.getByTestId('reference-pick-warning').textContent).toBe( + '选择的图片不是角色规范图,请选择生成规范里的角色规范。', + ); + + act(() => { + vi.advanceTimersByTime(10_000); + }); + + expect(screen.getByTestId('reference-pick-warning').textContent).toBe( + '选择的图片不是角色规范图,请选择生成规范里的角色规范。', + ); + } finally { + vi.useRealTimers(); + } + }); + it('restores the last picked character spec from local cache after remounting the editor', () => { const firstRender = render(); diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index bd11ed808..5d2bd23d9 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -189,7 +189,6 @@ const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastCharacterSpecReference'; const LAST_ICON_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastIconSpecReference'; -const REFERENCE_PICK_WARNING_DISMISS_MS = 2600; const INVALID_CHARACTER_SPEC_WARNING = '选择的图片不是角色规范图,请选择生成规范里的角色规范。'; const INVALID_ICON_SPEC_WARNING = @@ -697,16 +696,6 @@ export function useImageCanvasGenerationWorkflow({ generateDialog?.mode, ]); - useEffect(() => { - if (!referencePickWarning) { - return undefined; - } - const warningTimer = window.setTimeout(() => { - setReferencePickWarning(null); - }, REFERENCE_PICK_WARNING_DISMISS_MS); - return () => window.clearTimeout(warningTimer); - }, [referencePickWarning]); - const quickEditSourceLayer = quickEditPanel ? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ?? null) diff --git a/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx b/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx index ea78798fd..95e2ae19e 100644 --- a/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx +++ b/src/components/image-editor/useImageCanvasProjectPersistence.test.tsx @@ -89,9 +89,11 @@ function createDeferred() { function ProjectPersistenceHarness({ canAccessProtectedData = true, initialGenerationDialogs = [], + onProjectAccessLost, }: { canAccessProtectedData?: boolean; initialGenerationDialogs?: CanvasGenerationDialogState[]; + onProjectAccessLost?: () => void; }) { const [layers, setLayers] = useState([]); const [generationDialogs, setGenerationDialogs] = useState< @@ -151,6 +153,7 @@ function ProjectPersistenceHarness({ isViewportInteracting, canAccessProtectedData, openEditorLoginModal: openEditorLoginModalRef.current, + onProjectAccessLost, }); return ( @@ -1355,4 +1358,29 @@ describe('useImageCanvasProjectPersistence', () => { expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled(); }, ); + + it('notifies the shell to leave the canvas when an explicit project becomes inaccessible', async () => { + const onProjectAccessLost = vi.fn(); + window.history.replaceState( + null, + '', + '/editor/canvas?projectid=missing-project', + ); + loadEditorProjectMock.mockRejectedValueOnce( + new ApiClientError({ + message: '项目不存在', + status: 404, + code: 'not_found', + }), + ); + + render( + , + ); + + await waitFor(() => { + expect(onProjectAccessLost).toHaveBeenCalledTimes(1); + }); + expect(loadEditorProjectMock).toHaveBeenCalledWith('missing-project'); + }); }); diff --git a/src/components/image-editor/useImageCanvasProjectPersistence.ts b/src/components/image-editor/useImageCanvasProjectPersistence.ts index ff629c8e4..4fcd08227 100644 --- a/src/components/image-editor/useImageCanvasProjectPersistence.ts +++ b/src/components/image-editor/useImageCanvasProjectPersistence.ts @@ -78,6 +78,19 @@ type ImageCanvasProjectPersistenceSetters = { ) => void; }; +type ImageCanvasProjectPersistenceOptions = { + refs: ImageCanvasProjectPersistenceRefs; + setters: ImageCanvasProjectPersistenceSetters; + layers: CanvasLayer[]; + canvasGenerationDialogs: CanvasGenerationDialogState[]; + viewport: CanvasViewport; + isViewportInteracting: boolean; + canAccessProtectedData: boolean; + currentUserId?: string | null; + openEditorLoginModal: (postLoginAction?: (() => void) | null) => void; + onProjectAccessLost?: () => void; +}; + function isEditorAuthError(error: unknown) { return ( error instanceof ApiClientError && @@ -284,17 +297,8 @@ export function useImageCanvasProjectPersistence({ canAccessProtectedData, currentUserId, openEditorLoginModal, -}: { - refs: ImageCanvasProjectPersistenceRefs; - setters: ImageCanvasProjectPersistenceSetters; - layers: CanvasLayer[]; - canvasGenerationDialogs: CanvasGenerationDialogState[]; - viewport: CanvasViewport; - isViewportInteracting: boolean; - canAccessProtectedData: boolean; - currentUserId?: string | null; - openEditorLoginModal: (postLoginAction?: (() => void) | null) => void; -}) { + onProjectAccessLost, +}: ImageCanvasProjectPersistenceOptions) { const projectIdRef = useRef(null); const pendingProjectResourceLayersRef = useRef( [], @@ -704,6 +708,10 @@ export function useImageCanvasProjectPersistence({ } if (projectIdFromQuery && isEditorProjectAccessError(error)) { removeEditorProjectSessionCache(projectIdFromQuery); + if (onProjectAccessLost) { + onProjectAccessLost(); + return; + } replaceAppHistoryPath('/project'); } }); @@ -715,6 +723,7 @@ export function useImageCanvasProjectPersistence({ canAccessProtectedData, applyProjectSnapshot, createProjectResourceForLayer, + onProjectAccessLost, openEditorLoginModal, ]); diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index c54921efb..09e014b62 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -119,6 +119,7 @@ import { useHostNetworkOnline } from '../../hooks/useHostNetworkOnline'; import { buildPublicWorkStagePath, pushAppHistoryPath, + replaceAppHistoryPath, resolvePathForSelectionStage, } from '../../routing/appPageRoutes'; import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery'; @@ -3757,6 +3758,11 @@ export function PlatformEntryFlowShellImpl({ setSelectionStage('project'); }, [setSelectionStage]); + const replaceWithProjectGallery = useCallback(() => { + replaceAppHistoryPath('/project'); + setSelectionStage('project'); + }, [setSelectionStage]); + const openCreationCommunity = useCallback(() => { setIsCreationCommunityOpen(true); }, []); @@ -15716,7 +15722,9 @@ export function PlatformEntryFlowShellImpl({ } > - + )} diff --git a/src/index.css b/src/index.css index f741c6729..2954ef13a 100644 --- a/src/index.css +++ b/src/index.css @@ -8944,6 +8944,25 @@ button.image-canvas-editor__reference-chip:disabled { pointer-events: none; } +.image-canvas-editor__reference-pick-toast-close { + display: inline-grid; + width: 1.35rem; + height: 1.35rem; + place-items: center; + flex: 0 0 auto; + border: 0; + border-radius: 999px; + background: color-mix(in srgb, currentColor 16%, transparent); + color: inherit; + cursor: pointer; + pointer-events: auto; +} + +.image-canvas-editor__reference-pick-toast-close:hover, +.image-canvas-editor__reference-pick-toast-close:focus-visible { + background: color-mix(in srgb, currentColor 26%, transparent); +} + .image-canvas-editor__generate-submit { justify-self: end; }