feat: integrate jump-hop shelf and asset flow
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkMutationResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorksResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
@@ -41,6 +42,7 @@ export type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkMutationResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorksResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
};
|
||||
export type CreateJumpHopSessionRequest = {
|
||||
@@ -199,6 +201,17 @@ export async function getJumpHopGalleryDetail(publicWorkCode: string) {
|
||||
return normalizeJumpHopWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function listJumpHopWorks() {
|
||||
return requestJson<JumpHopWorksResponse>(
|
||||
JUMP_HOP_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取跳一跳作品列表失败',
|
||||
{
|
||||
retry: JUMP_HOP_RUNTIME_READ_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishJumpHopWork(profileId: string) {
|
||||
const response = await requestJson<JumpHopWorkMutationResponse>(
|
||||
`${JUMP_HOP_WORKS_API_BASE}/${encodeURIComponent(profileId)}/publish`,
|
||||
@@ -267,6 +280,7 @@ export const jumpHopClient = {
|
||||
getGalleryDetail: getJumpHopGalleryDetail,
|
||||
getWorkDetail: getJumpHopWorkDetail,
|
||||
listGallery: listJumpHopGallery,
|
||||
listWorks: listJumpHopWorks,
|
||||
publishWork: publishJumpHopWork,
|
||||
restartRun: restartJumpHopRuntimeRun,
|
||||
startRun: startJumpHopRuntimeRun,
|
||||
|
||||
24
src/services/puzzle-works/puzzleAssetClient.test.ts
Normal file
24
src/services/puzzle-works/puzzleAssetClient.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validatePuzzleReferenceImageFile,
|
||||
} from './puzzleAssetClient';
|
||||
|
||||
describe('puzzle reference image upload validation', () => {
|
||||
test('limits uploads to 6MB', () => {
|
||||
expect(PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES).toBe(6 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test('rejects files that exceed the upload limit with a precise message', () => {
|
||||
const file = new File([
|
||||
'x'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES + 1),
|
||||
], 'too-large.png', { type: 'image/png' });
|
||||
|
||||
expect(() => validatePuzzleReferenceImageFile(file)).toThrow(
|
||||
'参考图过大,请压缩后再上传(当前 6.0MB,最多 6MB)。',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
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;
|
||||
@@ -40,8 +44,6 @@ type ConfirmAssetObjectResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
const PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
|
||||
|
||||
const MIME_BY_EXTENSION: Record<string, string> = {
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
@@ -58,14 +60,9 @@ function resolvePuzzleImageContentType(file: File) {
|
||||
return MIME_BY_EXTENSION[extension] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
function validatePuzzleReferenceImageFile(file: File) {
|
||||
function validatePuzzleReferenceImageUploadFile(file: File) {
|
||||
const contentType = resolvePuzzleImageContentType(file);
|
||||
if (file.size <= 0) {
|
||||
throw new Error('参考图文件为空,请重新选择。');
|
||||
}
|
||||
if (file.size > PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES) {
|
||||
throw new Error('参考图过大,请压缩后再上传。');
|
||||
}
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
if (!contentType.startsWith('image/')) {
|
||||
throw new Error('参考图必须是图片文件。');
|
||||
}
|
||||
@@ -96,7 +93,7 @@ async function postDirectUploadFile(
|
||||
export async function uploadPuzzleReferenceImage(payload: {
|
||||
file: File;
|
||||
}): Promise<PuzzleReferenceAsset> {
|
||||
validatePuzzleReferenceImageFile(payload.file);
|
||||
validatePuzzleReferenceImageUploadFile(payload.file);
|
||||
const contentType = resolvePuzzleImageContentType(payload.file);
|
||||
const uploadedAt = Date.now();
|
||||
const ticket = await requestJson<DirectUploadTicketResponse>(
|
||||
@@ -157,7 +154,12 @@ export async function uploadPuzzleReferenceImage(payload: {
|
||||
|
||||
export const puzzleReferenceAssetTestUtils = {
|
||||
maxUploadBytes: PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validateFile: validatePuzzleReferenceImageFile,
|
||||
validateFile: validatePuzzleReferenceImageUploadFile,
|
||||
};
|
||||
|
||||
export {
|
||||
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validatePuzzleReferenceImageUploadFile as validatePuzzleReferenceImageFile,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('readPuzzleReferenceImageAsDataUrl', () => {
|
||||
const dataUrl = await readPuzzleReferenceImageAsDataUrl(file);
|
||||
|
||||
expect(dataUrl).toBe(`data:image/jpeg;base64,${'C'.repeat(1000)}`);
|
||||
expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 1536, 1152);
|
||||
expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 1024, 768);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.84);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.76);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.68);
|
||||
@@ -114,7 +114,7 @@ describe('readPuzzleReferenceImageAsDataUrl', () => {
|
||||
});
|
||||
|
||||
await expect(readPuzzleReferenceImageAsDataUrl(file)).rejects.toThrow(
|
||||
'参考图过大,请换一张尺寸更小的图片。',
|
||||
'参考图过大,请压缩后再上传(当前 10.0MB,最多 6MB)。',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
const PUZZLE_REFERENCE_IMAGE_MAX_EDGE = 1024;
|
||||
const PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES = 1536 * 1024;
|
||||
export const PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES = 6 * 1024 * 1024;
|
||||
export const PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH = 10 * 1024 * 1024;
|
||||
const PUZZLE_REFERENCE_IMAGE_SQUARE_TOLERANCE = 1;
|
||||
|
||||
export function formatPuzzleReferenceImageUploadBytes(bytes: number) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
export function buildPuzzleReferenceImageTooLargeMessage(actualBytes: number) {
|
||||
return `参考图过大,请压缩后再上传(当前 ${formatPuzzleReferenceImageUploadBytes(actualBytes)},最多 6MB)。`;
|
||||
}
|
||||
|
||||
export function validatePuzzleReferenceImageFile(file: File) {
|
||||
if (file.size <= 0) {
|
||||
throw new Error('参考图文件为空,请重新选择。');
|
||||
}
|
||||
if (file.size > PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES) {
|
||||
throw new Error(buildPuzzleReferenceImageTooLargeMessage(file.size));
|
||||
}
|
||||
if (file.type.trim() && !file.type.trim().startsWith('image/')) {
|
||||
throw new Error('参考图必须是图片文件。');
|
||||
}
|
||||
}
|
||||
|
||||
type PuzzleReferenceImageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -36,7 +57,7 @@ function readFileAsDataUrl(file: File) {
|
||||
|
||||
function ensureReferenceImageWithinLimit(dataUrl: string) {
|
||||
if (dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) {
|
||||
throw new Error('参考图过大,请换一张尺寸更小的图片。');
|
||||
throw new Error(buildPuzzleReferenceImageTooLargeMessage(dataUrl.length));
|
||||
}
|
||||
return dataUrl;
|
||||
}
|
||||
@@ -130,6 +151,7 @@ async function compressReferenceImageDataUrl(file: File, dataUrl: string) {
|
||||
}
|
||||
|
||||
export async function readPuzzleReferenceImageAsDataUrl(file: File) {
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
try {
|
||||
const compressedDataUrl = await compressReferenceImageDataUrl(
|
||||
@@ -150,6 +172,7 @@ export async function readPuzzleReferenceImageAsDataUrl(file: File) {
|
||||
export async function readPuzzleReferenceImageForUpload(
|
||||
file: File,
|
||||
): Promise<PuzzleReferenceImageReadResult> {
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
const image = await loadReferenceImage(dataUrl);
|
||||
const size = resolveReferenceImageNaturalSize(image);
|
||||
|
||||
Reference in New Issue
Block a user