188 lines
5.3 KiB
TypeScript
188 lines
5.3 KiB
TypeScript
import { ASSET_API_PATHS } from '../../editor/shared/editorApiClient';
|
|
import { requestJson } from '../apiClient';
|
|
import {
|
|
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
|
validatePuzzleReferenceImageFile,
|
|
} from '../puzzleReferenceImage';
|
|
|
|
export type PuzzleHistoryAsset = {
|
|
assetObjectId: string;
|
|
assetKind: 'puzzle_cover_image';
|
|
imageSrc: string;
|
|
ownerUserId?: string | null;
|
|
ownerLabel: string;
|
|
profileId?: string | null;
|
|
entityId?: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type PuzzleReferenceAsset = PuzzleHistoryAsset & {
|
|
objectKey: string;
|
|
};
|
|
|
|
type DirectUploadTicketResponse = {
|
|
upload: {
|
|
bucket: string;
|
|
host: string;
|
|
objectKey: string;
|
|
legacyPublicPath: string;
|
|
formFields: Record<string, string | null | undefined>;
|
|
};
|
|
};
|
|
|
|
type ConfirmAssetObjectResponse = {
|
|
assetObject: {
|
|
assetObjectId: string;
|
|
objectKey: string;
|
|
assetKind: 'puzzle_cover_image';
|
|
ownerUserId?: string | null;
|
|
profileId?: string | null;
|
|
entityId?: string | null;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
};
|
|
};
|
|
|
|
const MIME_BY_EXTENSION: Record<string, string> = {
|
|
jpeg: 'image/jpeg',
|
|
jpg: 'image/jpeg',
|
|
png: 'image/png',
|
|
webp: 'image/webp',
|
|
};
|
|
|
|
function resolvePuzzleImageContentType(file: File) {
|
|
if (file.type.trim()) {
|
|
return file.type.trim();
|
|
}
|
|
|
|
const extension = file.name.split('.').pop()?.trim().toLowerCase() ?? '';
|
|
return MIME_BY_EXTENSION[extension] ?? 'application/octet-stream';
|
|
}
|
|
|
|
function validatePuzzleReferenceImageUploadFile(file: File) {
|
|
const contentType = resolvePuzzleImageContentType(file);
|
|
validatePuzzleReferenceImageFile(file);
|
|
if (!contentType.startsWith('image/')) {
|
|
throw new Error('参考图必须是图片文件。');
|
|
}
|
|
}
|
|
|
|
async function postDirectUploadFile(
|
|
upload: DirectUploadTicketResponse['upload'],
|
|
file: File,
|
|
) {
|
|
const formData = new FormData();
|
|
Object.entries(upload.formFields).forEach(([key, value]) => {
|
|
if (value !== null && value !== undefined) {
|
|
formData.append(key, value);
|
|
}
|
|
});
|
|
formData.append('file', file);
|
|
|
|
const response = await fetch(upload.host, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('上传拼图参考图失败。');
|
|
}
|
|
}
|
|
|
|
export async function uploadPuzzleReferenceImage(payload: {
|
|
file: File;
|
|
}): Promise<PuzzleReferenceAsset> {
|
|
validatePuzzleReferenceImageUploadFile(payload.file);
|
|
const contentType = resolvePuzzleImageContentType(payload.file);
|
|
const uploadedAt = Date.now();
|
|
const ticket = await requestJson<DirectUploadTicketResponse>(
|
|
'/api/assets/direct-upload-tickets',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
legacyPrefix: 'generated-puzzle-assets',
|
|
pathSegments: ['puzzle-reference', 'draft', `${uploadedAt}`],
|
|
fileName: payload.file.name,
|
|
contentType,
|
|
access: 'private',
|
|
maxSizeBytes: PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
|
metadata: {
|
|
asset_kind: 'puzzle_cover_image',
|
|
puzzle_slot: 'reference_image',
|
|
},
|
|
}),
|
|
},
|
|
'创建拼图参考图上传凭证失败',
|
|
);
|
|
|
|
await postDirectUploadFile(ticket.upload, payload.file);
|
|
|
|
const confirmed = await requestJson<ConfirmAssetObjectResponse>(
|
|
'/api/assets/objects/confirm',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
bucket: ticket.upload.bucket,
|
|
objectKey: ticket.upload.objectKey,
|
|
contentType,
|
|
contentLength: payload.file.size,
|
|
assetKind: 'puzzle_cover_image',
|
|
accessPolicy: 'private',
|
|
}),
|
|
},
|
|
'确认拼图参考图失败',
|
|
);
|
|
|
|
return {
|
|
assetObjectId: confirmed.assetObject.assetObjectId,
|
|
assetKind: confirmed.assetObject.assetKind,
|
|
objectKey: confirmed.assetObject.objectKey,
|
|
imageSrc: ticket.upload.legacyPublicPath,
|
|
ownerUserId: confirmed.assetObject.ownerUserId,
|
|
ownerLabel: confirmed.assetObject.ownerUserId
|
|
? `账号 ${confirmed.assetObject.ownerUserId}`
|
|
: '当前账号',
|
|
profileId: confirmed.assetObject.profileId,
|
|
entityId: confirmed.assetObject.entityId,
|
|
createdAt: confirmed.assetObject.createdAt ?? '',
|
|
updatedAt: confirmed.assetObject.updatedAt ?? '',
|
|
};
|
|
}
|
|
|
|
export const puzzleReferenceAssetTestUtils = {
|
|
maxUploadBytes: PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
|
validateFile: validatePuzzleReferenceImageUploadFile,
|
|
};
|
|
|
|
export {
|
|
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
|
validatePuzzleReferenceImageUploadFile as validatePuzzleReferenceImageFile,
|
|
};
|
|
|
|
/**
|
|
* 读取历史拼图图片素材。结果页只把它们作为参考图来源,
|
|
* 不直接替换当前正式图,正式图仍由后端单图生成链路写回。
|
|
*/
|
|
export async function listPuzzleHistoryAssets(payload: { limit?: number }) {
|
|
const params = new URLSearchParams({ kind: 'puzzle_cover_image' });
|
|
if (payload.limit) {
|
|
params.set('limit', String(payload.limit));
|
|
}
|
|
|
|
const response = await requestJson<{ assets: PuzzleHistoryAsset[] }>(
|
|
`${ASSET_API_PATHS.assetHistory}?${params.toString()}`,
|
|
{ method: 'GET' },
|
|
'读取历史拼图素材失败',
|
|
);
|
|
|
|
return response.assets;
|
|
}
|
|
|
|
export const puzzleAssetClient = {
|
|
listHistoryAssets: listPuzzleHistoryAssets,
|
|
uploadReferenceImage: uploadPuzzleReferenceImage,
|
|
};
|