抽 buildGamePublishMediaSubmission,发布与编辑共用同一份媒体提交
- gamePublishAssets 新增媒体提交合成:槽位 objectKey 去空白、本地新图只进 parts - GamePublishPage 的 metadata 槽位与 multipart part、GameWorkMetadataEditor 的 payload/media 都改用它 - 补槽位与 part 同源的单测
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
resolveGameInputModes,
|
||||
} from './gameMetadataOptions';
|
||||
import {
|
||||
buildGamePublishMediaSubmission,
|
||||
type GamePublishImage,
|
||||
revokeGamePublishImagePreview,
|
||||
} from './gamePublishAssets';
|
||||
@@ -312,19 +313,19 @@ export function GamePublishPage({
|
||||
* 组装提交给服务端的资料 DTO。
|
||||
*
|
||||
* 媒体槽位只描述来源:本地新图写 `null`(由对应的二进制 part 补位),沿用线上图写 objectKey;
|
||||
* 原始字节另由 `buildPublishMediaParts` 作为 multipart part 提交。
|
||||
* 原始字节另由 `buildPublishMediaParts` 作为 multipart part 提交。两者共用
|
||||
* `buildGamePublishMediaSubmission`,槽位映射与 part 组装不会各改各的。
|
||||
*/
|
||||
function buildPublishGameMetadata() {
|
||||
const media = buildGamePublishMediaSubmission(coverImage, screenshotImages);
|
||||
return {
|
||||
title: title.trim(),
|
||||
summary: summary.trim(),
|
||||
description: description.trim() || summary.trim(),
|
||||
category,
|
||||
tags: parseGameTagsInput(tagsInput),
|
||||
coverObjectKey: coverImage?.file ? null : (coverImage?.objectKey ?? null),
|
||||
screenshots: screenshotImages.map((image) =>
|
||||
image.file ? null : (image.objectKey ?? null),
|
||||
),
|
||||
coverObjectKey: media.coverObjectKey,
|
||||
screenshots: media.screenshots,
|
||||
deviceSupport,
|
||||
inputModes: resolveGameInputModes(deviceSupport),
|
||||
orientation,
|
||||
@@ -332,10 +333,7 @@ export function GamePublishPage({
|
||||
}
|
||||
|
||||
function buildPublishMediaParts() {
|
||||
return {
|
||||
cover: coverImage?.file ?? null,
|
||||
screenshots: screenshotImages.map((image) => image.file ?? null),
|
||||
};
|
||||
return buildGamePublishMediaSubmission(coverImage, screenshotImages).parts;
|
||||
}
|
||||
|
||||
/** 是否已有封面:本地新图或沿用线上图都算。 */
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
resolveGameInputModes,
|
||||
} from './gameMetadataOptions';
|
||||
import {
|
||||
buildGamePublishMediaSubmission,
|
||||
type GamePublishImage,
|
||||
revokeGamePublishImagePreview,
|
||||
} from './gamePublishAssets';
|
||||
@@ -176,10 +177,12 @@ export function GameWorkMetadataEditor({
|
||||
}
|
||||
const saveKey = saveKeyRef.current ?? createMetadataKey(game.id);
|
||||
saveKeyRef.current = saveKey;
|
||||
const media = {
|
||||
cover: coverImage?.file ?? null,
|
||||
screenshots: screenshotImages.map((image) => image.file ?? null),
|
||||
};
|
||||
// 槽位映射与二进制 part 同源,和发布页共用同一份合成规则。
|
||||
const submission = buildGamePublishMediaSubmission(
|
||||
coverImage,
|
||||
screenshotImages,
|
||||
);
|
||||
const media = submission.parts;
|
||||
const payload: GameDistributionUpdateGameMetadataRequest = {
|
||||
expectedPublicationRevision: game.publicationRevision,
|
||||
title: title.trim(),
|
||||
@@ -187,12 +190,8 @@ export function GameWorkMetadataEditor({
|
||||
description: description.trim() || summary.trim(),
|
||||
category,
|
||||
tags: parseGameTagsInput(tagsInput),
|
||||
coverObjectKey: coverImage?.file
|
||||
? null
|
||||
: (coverImage?.objectKey?.trim() ?? null),
|
||||
screenshots: screenshotImages.map((image) =>
|
||||
image.file ? null : (image.objectKey?.trim() ?? null),
|
||||
),
|
||||
coverObjectKey: submission.coverObjectKey,
|
||||
screenshots: submission.screenshots,
|
||||
deviceSupport,
|
||||
inputModes: resolveGameInputModes(deviceSupport),
|
||||
orientation,
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createLocalGamePublishImage } from './gamePublishAssets';
|
||||
import {
|
||||
buildGamePublishMediaSubmission,
|
||||
createLocalGamePublishImage,
|
||||
} from './gamePublishAssets';
|
||||
|
||||
describe('createLocalGamePublishImage', () => {
|
||||
it('本地新选图各自带稳定且不同的 id', () => {
|
||||
@@ -21,3 +24,43 @@ describe('createLocalGamePublishImage', () => {
|
||||
expect(first.id).not.toBe(second.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildGamePublishMediaSubmission', () => {
|
||||
it('本地新图只进 parts、槽位留 null,沿用线上图反之', () => {
|
||||
const localCover = createLocalGamePublishImage(
|
||||
new File(['a'], 'cover.png', { type: 'image/png' }),
|
||||
'cover',
|
||||
);
|
||||
const onlineCover = {
|
||||
id: 'online-cover',
|
||||
objectKey: ' agc/cover.png ',
|
||||
src: 'https://cdn/cover.png',
|
||||
name: '线上封面',
|
||||
};
|
||||
const localShot = createLocalGamePublishImage(
|
||||
new File(['b'], 'shot.png', { type: 'image/png' }),
|
||||
'screenshot',
|
||||
);
|
||||
const onlineShot = {
|
||||
id: 'online-shot',
|
||||
objectKey: ' ',
|
||||
src: '',
|
||||
name: '截图 1',
|
||||
};
|
||||
|
||||
const submission = buildGamePublishMediaSubmission(onlineCover, [
|
||||
localShot,
|
||||
onlineShot,
|
||||
]);
|
||||
|
||||
// 槽位与 part 必须同源:封面沿用时 objectKey 去空白,本地截图占 null 槽位。
|
||||
expect(submission.coverObjectKey).toBe('agc/cover.png');
|
||||
expect(submission.parts.cover).toBeNull();
|
||||
expect(submission.screenshots).toEqual([null, null]);
|
||||
expect(submission.parts.screenshots).toEqual([localShot.file, null]);
|
||||
|
||||
const localSubmission = buildGamePublishMediaSubmission(localCover, []);
|
||||
expect(localSubmission.coverObjectKey).toBeNull();
|
||||
expect(localSubmission.parts.cover).toBe(localCover.file);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,6 +99,40 @@ export function createLocalGamePublishImage(
|
||||
};
|
||||
}
|
||||
|
||||
/** 一次发布/编辑提交里的媒体:metadata 槽位与随请求提交的二进制 part。 */
|
||||
export type GamePublishMediaSubmission = {
|
||||
/** 沿用线上封面时的 objectKey;本次带本地新封面时为 `null`。 */
|
||||
coverObjectKey: string | null;
|
||||
/** 与最终截图顺序一一对应:`string` 沿用线上,`null` 表示消费对应的本地 `parts`。 */
|
||||
screenshots: (string | null)[];
|
||||
parts: {
|
||||
cover: File | null;
|
||||
screenshots: (File | null)[];
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 把表单里的封面/截图列表合成一份提交:metadata 槽位与 multipart part 同源。
|
||||
*
|
||||
* 「本地新图 / 沿用线上」是二选一:有 `file` 就归到 `parts` 并把槽位置 `null`,否则槽位取
|
||||
* 去空白后的 `objectKey`。发布与编辑两条链路共用这一份,避免槽位映射或 part 组装各改各的。
|
||||
*/
|
||||
export function buildGamePublishMediaSubmission(
|
||||
cover: GamePublishImage | null,
|
||||
screenshots: GamePublishImage[],
|
||||
): GamePublishMediaSubmission {
|
||||
const objectKeyOf = (image: GamePublishImage | null | undefined) =>
|
||||
image?.file ? null : image?.objectKey?.trim() || null;
|
||||
return {
|
||||
coverObjectKey: objectKeyOf(cover),
|
||||
screenshots: screenshots.map(objectKeyOf),
|
||||
parts: {
|
||||
cover: cover?.file ?? null,
|
||||
screenshots: screenshots.map((image) => image.file ?? null),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 释放本地预览的 blob URL;远程签名地址与空地址直接忽略。 */
|
||||
export function revokeGamePublishImagePreview(
|
||||
image: GamePublishImage | null | undefined,
|
||||
|
||||
Reference in New Issue
Block a user