diff --git a/src/components/game-distribution/GamePublishPage.tsx b/src/components/game-distribution/GamePublishPage.tsx index 7ee3da47f..d059aed92 100644 --- a/src/components/game-distribution/GamePublishPage.tsx +++ b/src/components/game-distribution/GamePublishPage.tsx @@ -203,6 +203,8 @@ export function GamePublishPage({ title: detail.game.title, }); void applyVersionMediaDefaults(detail, { + getCover: () => coverImageRef.current, + getScreenshots: () => screenshotImagesRef.current, setCover: setCoverImage, setScreenshots: setScreenshotImages, isCancelled: () => cancelled, @@ -285,6 +287,8 @@ export function GamePublishPage({ setPriceInput(''); } void applyVersionMediaDefaults(detail, { + getCover: () => coverImageRef.current, + getScreenshots: () => screenshotImagesRef.current, setCover: setCoverImage, setScreenshots: setScreenshotImages, isCancelled: () => cancelled, diff --git a/src/components/game-distribution/GameWorkMetadataEditor.tsx b/src/components/game-distribution/GameWorkMetadataEditor.tsx index 9bfdad4e6..cdc8f053a 100644 --- a/src/components/game-distribution/GameWorkMetadataEditor.tsx +++ b/src/components/game-distribution/GameWorkMetadataEditor.tsx @@ -132,6 +132,8 @@ export function GameWorkMetadataEditor({ .then((detail) => { if (cancelled) return; return applyVersionMediaDefaults(detail, { + getCover: () => coverImageRef.current, + getScreenshots: () => screenshotImagesRef.current, setCover: setCoverImage, setScreenshots: setScreenshotImages, isCancelled: () => cancelled, diff --git a/src/components/game-distribution/gamePublishMediaDefaults.test.ts b/src/components/game-distribution/gamePublishMediaDefaults.test.ts index b3c1ed84e..20a051c79 100644 --- a/src/components/game-distribution/gamePublishMediaDefaults.test.ts +++ b/src/components/game-distribution/gamePublishMediaDefaults.test.ts @@ -44,6 +44,8 @@ describe('applyVersionMediaDefaults', () => { } as unknown as GameDistributionVersionDetail; await applyVersionMediaDefaults(detail, { + getCover: () => cover, + getScreenshots: () => screenshots, setCover: (updater) => { cover = updater(cover); }, diff --git a/src/components/game-distribution/gamePublishMediaDefaults.ts b/src/components/game-distribution/gamePublishMediaDefaults.ts index 2e52f2399..4ca714af0 100644 --- a/src/components/game-distribution/gamePublishMediaDefaults.ts +++ b/src/components/game-distribution/gamePublishMediaDefaults.ts @@ -7,6 +7,13 @@ import { } from './gamePublishAssets'; type VersionMediaApplyTarget = { + /** + * 调用瞬间的当前封面。用 ref 读最新值而不是接收 updater:释放 blob URL 是副作用,放进 + * `setState` 的 updater 里会被 StrictMode 双调用/重放,撤销到仍被渲染引用的 URL。 + */ + getCover: () => GamePublishImage | null; + /** 调用瞬间的当前截图,理由同 {@link getCover}。 */ + getScreenshots: () => GamePublishImage[]; setCover: ( updater: (current: GamePublishImage | null) => GamePublishImage | null, ) => void; @@ -48,19 +55,20 @@ export async function applyVersionMediaDefaults( .filter(Boolean); if (apply.isCancelled()) return; // 覆盖前先释放被替换掉的本地预览 blob URL:这一步之后旧对象不再有人引用,不 revoke 就泄漏。 - apply.setCover((current) => { - revokeGamePublishImagePreview(current); - return coverObjectKey ? buildCover('') : null; - }); - apply.setScreenshots((current) => { - for (const image of current) revokeGamePublishImagePreview(image); - return screenshotObjectKeys.map((objectKey, index) => ({ + // revoke 在 dispatch 之前做,updater 只负责纯计算,重放不会撤销到仍在用的 URL。 + revokeGamePublishImagePreview(apply.getCover()); + for (const image of apply.getScreenshots()) { + revokeGamePublishImagePreview(image); + } + apply.setCover(() => (coverObjectKey ? buildCover('') : null)); + apply.setScreenshots(() => + screenshotObjectKeys.map((objectKey, index) => ({ id: createGamePublishImageId(), objectKey, src: '', name: `截图 ${index + 1}`, - })); - }); + })), + ); const coverPreview = await resolveGamePublishImagePreview( coverObjectKey,