This commit is contained in:
@@ -64,9 +64,7 @@ import type {
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type {
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import type { SquareHoleRunSnapshot } from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import type {
|
||||
SquareHoleWorkProfile,
|
||||
SquareHoleWorkSummary,
|
||||
@@ -1703,7 +1701,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshBigFishGallery();
|
||||
openPublishShareModal({
|
||||
title: response.session.draft?.title ?? '大鱼吃小鱼',
|
||||
publicWorkCode: buildBigFishPublicWorkCode(response.session.sessionId),
|
||||
publicWorkCode: buildBigFishPublicWorkCode(
|
||||
response.session.sessionId,
|
||||
),
|
||||
stage: 'big-fish-runtime',
|
||||
});
|
||||
}
|
||||
@@ -1889,8 +1889,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'ready',
|
||||
completedAssetCount: item.shapeOptions.length + 2,
|
||||
totalAssetCount: item.shapeOptions.length + 2,
|
||||
completedAssetCount:
|
||||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||||
totalAssetCount:
|
||||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||||
error: null,
|
||||
}));
|
||||
await refreshSquareHoleShelf().catch(() => undefined);
|
||||
@@ -1906,8 +1908,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
phase: 'failed',
|
||||
error: errorMessage,
|
||||
}));
|
||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||
setSelectionStage('square-hole-generating');
|
||||
setSquareHoleProfile(
|
||||
buildSquareHoleProfileFromSession(response.session),
|
||||
);
|
||||
setSelectionStage('square-hole-result');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1918,14 +1922,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'ready',
|
||||
completedAssetCount: item.shapeOptions.length + 2,
|
||||
totalAssetCount: item.shapeOptions.length + 2,
|
||||
completedAssetCount:
|
||||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||||
totalAssetCount:
|
||||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||||
error: null,
|
||||
}));
|
||||
await refreshSquareHoleShelf().catch(() => undefined);
|
||||
setSelectionStage('square-hole-result');
|
||||
} catch {
|
||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||
setSquareHoleProfile(
|
||||
buildSquareHoleProfileFromSession(response.session),
|
||||
);
|
||||
setSelectionStage('square-hole-result');
|
||||
}
|
||||
},
|
||||
@@ -2031,7 +2039,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
openPublishShareModal({
|
||||
title: galleryDetail.item.workTitle || galleryDetail.item.levelName,
|
||||
publicWorkCode: buildPuzzlePublicWorkCode(galleryDetail.item.profileId),
|
||||
publicWorkCode: buildPuzzlePublicWorkCode(
|
||||
galleryDetail.item.profileId,
|
||||
),
|
||||
stage: 'puzzle-gallery-detail',
|
||||
});
|
||||
}
|
||||
@@ -2087,8 +2097,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
const setSquareHoleError = squareHoleFlow.setError;
|
||||
const isSquareHoleBusy = squareHoleFlow.isBusy;
|
||||
const streamingSquareHoleReplyText = squareHoleFlow.streamingReplyText;
|
||||
const setStreamingSquareHoleReplyText =
|
||||
squareHoleFlow.setStreamingReplyText;
|
||||
const setStreamingSquareHoleReplyText = squareHoleFlow.setStreamingReplyText;
|
||||
const isStreamingSquareHoleReply = squareHoleFlow.isStreamingReply;
|
||||
const setIsStreamingSquareHoleReply = squareHoleFlow.setIsStreamingReply;
|
||||
|
||||
@@ -2310,10 +2319,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const handleCreationHubCreateType = useCallback(
|
||||
(type: PlatformCreationTypeId) => {
|
||||
if (
|
||||
type === 'airp' ||
|
||||
type === 'visual-novel'
|
||||
) {
|
||||
if (type === 'airp' || type === 'visual-novel') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2796,9 +2802,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRuntimeReturnStage('puzzle-result');
|
||||
setSelectionStage('puzzle-runtime');
|
||||
} catch (error) {
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'),
|
||||
);
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'));
|
||||
} finally {
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
@@ -2885,7 +2889,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
|
||||
const dragPuzzlePiece = useCallback(
|
||||
async (payload: { pieceId: string; targetRow: number; targetCol: number }) => {
|
||||
async (payload: {
|
||||
pieceId: string;
|
||||
targetRow: number;
|
||||
targetCol: number;
|
||||
}) => {
|
||||
if (!puzzleRun || isPuzzleBusy) {
|
||||
return;
|
||||
}
|
||||
@@ -3014,7 +3022,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
puzzleRunRef.current = run;
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。'));
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。'),
|
||||
);
|
||||
} finally {
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
@@ -3220,45 +3230,50 @@ export function PlatformEntryFlowShellImpl({
|
||||
],
|
||||
);
|
||||
|
||||
const remodelCurrentPuzzleRuntimeWork = useCallback((profileId: string) => {
|
||||
const targetProfileId = profileId.trim();
|
||||
if (!targetProfileId || isPublicWorkDetailBusy || isPuzzleBusy) {
|
||||
return;
|
||||
}
|
||||
const remodelCurrentPuzzleRuntimeWork = useCallback(
|
||||
(profileId: string) => {
|
||||
const targetProfileId = profileId.trim();
|
||||
if (!targetProfileId || isPublicWorkDetailBusy || isPuzzleBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
runProtectedAction(() => {
|
||||
setIsPublicWorkDetailBusy(true);
|
||||
setIsPuzzleBusy(true);
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
runProtectedAction(() => {
|
||||
setIsPublicWorkDetailBusy(true);
|
||||
setIsPuzzleBusy(true);
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
|
||||
void remixPuzzleGalleryWork(targetProfileId)
|
||||
.then((response) => {
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleOperation(null);
|
||||
setPuzzleRun(null);
|
||||
enterCreateTab();
|
||||
setSelectionStage('puzzle-result');
|
||||
})
|
||||
.catch((error) => {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '改造拼图作品失败。'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPublicWorkDetailBusy(false);
|
||||
setIsPuzzleBusy(false);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
enterCreateTab,
|
||||
isPublicWorkDetailBusy,
|
||||
isPuzzleBusy,
|
||||
puzzleFlow,
|
||||
resolvePuzzleErrorMessage,
|
||||
runProtectedAction,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
setSelectionStage,
|
||||
]);
|
||||
void remixPuzzleGalleryWork(targetProfileId)
|
||||
.then((response) => {
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleOperation(null);
|
||||
setPuzzleRun(null);
|
||||
enterCreateTab();
|
||||
setSelectionStage('puzzle-result');
|
||||
})
|
||||
.catch((error) => {
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '改造拼图作品失败。'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPublicWorkDetailBusy(false);
|
||||
setIsPuzzleBusy(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
[
|
||||
enterCreateTab,
|
||||
isPublicWorkDetailBusy,
|
||||
isPuzzleBusy,
|
||||
puzzleFlow,
|
||||
resolvePuzzleErrorMessage,
|
||||
runProtectedAction,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const leaveAgentWorkspace = useCallback(() => {
|
||||
enterCreateTab();
|
||||
@@ -4134,7 +4149,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
const { item: profile } = await getSquareHoleWorkDetail(item.profileId);
|
||||
setSquareHoleProfile(profile);
|
||||
} catch (error) {
|
||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(restoredSession));
|
||||
setSquareHoleProfile(
|
||||
buildSquareHoleProfileFromSession(restoredSession),
|
||||
);
|
||||
setSquareHoleError(
|
||||
resolveSquareHoleErrorMessage(error, '读取方洞挑战作品详情失败。'),
|
||||
);
|
||||
@@ -5701,7 +5718,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载方洞挑战生成面板..." />}
|
||||
fallback={
|
||||
<LazyPanelFallback label="正在加载方洞挑战生成面板..." />
|
||||
}
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={
|
||||
@@ -5736,59 +5755,60 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'square-hole-result' && squareHoleSession?.draft && (
|
||||
<motion.div
|
||||
key="square-hole-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载方洞挑战结果..." />}
|
||||
{selectionStage === 'square-hole-result' &&
|
||||
squareHoleSession?.draft && (
|
||||
<motion.div
|
||||
key="square-hole-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<SquareHoleResultView
|
||||
profile={
|
||||
squareHoleProfile ??
|
||||
buildSquareHoleProfileFromSession(squareHoleSession)!
|
||||
}
|
||||
draft={squareHoleSession.draft}
|
||||
isBusy={isSquareHoleBusy}
|
||||
error={squareHoleError}
|
||||
onBack={() => {
|
||||
setSelectionStage('square-hole-agent-workspace');
|
||||
}}
|
||||
onSaved={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
}}
|
||||
onPublished={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
void Promise.allSettled([
|
||||
refreshSquareHoleShelf(),
|
||||
refreshSquareHoleGallery(),
|
||||
]);
|
||||
openPublicWorkDetail(
|
||||
mapSquareHoleWorkToPublicWorkDetail(profile),
|
||||
);
|
||||
openPublishShareModal({
|
||||
title: profile.gameName,
|
||||
publicWorkCode: buildSquareHolePublicWorkCode(
|
||||
profile.profileId,
|
||||
),
|
||||
stage: 'work-detail',
|
||||
});
|
||||
}}
|
||||
onStartTestRun={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
void startSquareHoleRunFromProfile(
|
||||
profile,
|
||||
'square-hole-result',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载方洞挑战结果..." />}
|
||||
>
|
||||
<SquareHoleResultView
|
||||
profile={
|
||||
squareHoleProfile ??
|
||||
buildSquareHoleProfileFromSession(squareHoleSession)!
|
||||
}
|
||||
draft={squareHoleSession.draft}
|
||||
isBusy={isSquareHoleBusy}
|
||||
error={squareHoleError}
|
||||
onBack={() => {
|
||||
setSelectionStage('square-hole-agent-workspace');
|
||||
}}
|
||||
onSaved={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
}}
|
||||
onPublished={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
void Promise.allSettled([
|
||||
refreshSquareHoleShelf(),
|
||||
refreshSquareHoleGallery(),
|
||||
]);
|
||||
openPublicWorkDetail(
|
||||
mapSquareHoleWorkToPublicWorkDetail(profile),
|
||||
);
|
||||
openPublishShareModal({
|
||||
title: profile.gameName,
|
||||
publicWorkCode: buildSquareHolePublicWorkCode(
|
||||
profile.profileId,
|
||||
),
|
||||
stage: 'work-detail',
|
||||
});
|
||||
}}
|
||||
onStartTestRun={(profile) => {
|
||||
setSquareHoleProfile(profile);
|
||||
void startSquareHoleRunFromProfile(
|
||||
profile,
|
||||
'square-hole-result',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'square-hole-runtime' && (
|
||||
<motion.div
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
DropSquareHoleShapeRequest,
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import { SquareHoleRuntimeShell } from './SquareHoleRuntimeShell';
|
||||
|
||||
function buildRun(): SquareHoleRunSnapshot {
|
||||
return {
|
||||
runId: 'run-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
status: 'running',
|
||||
snapshotVersion: 3,
|
||||
startedAtMs: Date.now(),
|
||||
durationLimitMs: 60_000,
|
||||
remainingMs: 60_000,
|
||||
totalShapeCount: 8,
|
||||
completedShapeCount: 0,
|
||||
combo: 0,
|
||||
bestCombo: 0,
|
||||
score: 0,
|
||||
ruleLabel: '把当前选项投入指定洞口',
|
||||
backgroundImageSrc: null,
|
||||
currentShape: {
|
||||
shapeId: 'shape-1',
|
||||
shapeKind: 'square',
|
||||
label: '当前选项',
|
||||
targetHoleId: 'hole-b',
|
||||
color: '#38bdf8',
|
||||
imageSrc: null,
|
||||
},
|
||||
holes: [
|
||||
{
|
||||
holeId: 'hole-a',
|
||||
holeKind: 'hole-a',
|
||||
label: '洞口 A',
|
||||
x: 0.28,
|
||||
y: 0.32,
|
||||
imageSrc: null,
|
||||
},
|
||||
{
|
||||
holeId: 'hole-b',
|
||||
holeKind: 'hole-b',
|
||||
label: '洞口 B',
|
||||
x: 0.66,
|
||||
y: 0.52,
|
||||
imageSrc: null,
|
||||
},
|
||||
],
|
||||
lastFeedback: null,
|
||||
};
|
||||
}
|
||||
|
||||
function renderRuntime() {
|
||||
const run = buildRun();
|
||||
const onDropShape = vi.fn(async (_payload: DropSquareHoleShapeRequest) => ({
|
||||
feedback: {
|
||||
accepted: true,
|
||||
rejectReason: null,
|
||||
message: '已投入',
|
||||
},
|
||||
run,
|
||||
}));
|
||||
|
||||
render(
|
||||
<SquareHoleRuntimeShell
|
||||
run={run}
|
||||
onBack={vi.fn()}
|
||||
onRestart={vi.fn()}
|
||||
onDropShape={onDropShape}
|
||||
/>,
|
||||
);
|
||||
|
||||
return { onDropShape };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('点击洞口会提交该洞口选择', async () => {
|
||||
const { onDropShape } = renderRuntime();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '投入 洞口 B' }));
|
||||
|
||||
await waitFor(() => expect(onDropShape).toHaveBeenCalledTimes(1));
|
||||
expect(onDropShape.mock.calls[0]?.[0]).toMatchObject({
|
||||
runId: 'run-1',
|
||||
holeId: 'hole-b',
|
||||
clientSnapshotVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('引导高亮不会默认指向当前正确洞口', () => {
|
||||
renderRuntime();
|
||||
|
||||
const correctHole = screen.getByRole('button', { name: '投入 洞口 B' });
|
||||
const hintedHole = screen.getByRole('button', { name: '投入 洞口 A' });
|
||||
|
||||
expect(correctHole.className).not.toContain('ring-2');
|
||||
expect(hintedHole.className).toContain('ring-2');
|
||||
});
|
||||
|
||||
test('拖拽当前选项到洞口上松开会提交该洞口选择', async () => {
|
||||
const { onDropShape } = renderRuntime();
|
||||
const shape = screen.getByRole('button', { name: '拖拽当前选项' });
|
||||
const hole = screen.getByRole('button', { name: '投入 洞口 A' });
|
||||
const elementsFromPoint = vi.fn(() => [hole]);
|
||||
Object.defineProperty(document, 'elementsFromPoint', {
|
||||
configurable: true,
|
||||
value: elementsFromPoint,
|
||||
});
|
||||
|
||||
fireEvent.pointerDown(shape, { pointerId: 1, clientX: 20, clientY: 20 });
|
||||
fireEvent.pointerMove(shape, { pointerId: 1, clientX: 140, clientY: 160 });
|
||||
fireEvent.pointerUp(shape, { pointerId: 1, clientX: 140, clientY: 160 });
|
||||
|
||||
await waitFor(() => expect(onDropShape).toHaveBeenCalledTimes(1));
|
||||
expect(onDropShape.mock.calls[0]?.[0]).toMatchObject({
|
||||
runId: 'run-1',
|
||||
holeId: 'hole-a',
|
||||
clientSnapshotVersion: 3,
|
||||
});
|
||||
expect(elementsFromPoint).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,18 +1,26 @@
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
Image,
|
||||
RotateCcw,
|
||||
Shapes,
|
||||
Sparkles,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
DropSquareHoleShapeRequest,
|
||||
SquareHoleDropResponse,
|
||||
SquareHoleHoleSnapshot,
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
@@ -35,6 +43,16 @@ type PendingDrop = {
|
||||
holeId: string;
|
||||
};
|
||||
|
||||
type DragState = {
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
moved: boolean;
|
||||
};
|
||||
|
||||
function isRunning(run: SquareHoleRunSnapshot) {
|
||||
return run.status.toLowerCase() === 'running';
|
||||
}
|
||||
@@ -52,44 +70,17 @@ function buildClientEventId(runId: string, holeId: string) {
|
||||
)}`;
|
||||
}
|
||||
|
||||
function getHoleShapeClass(hole: SquareHoleHoleSnapshot) {
|
||||
const kind = hole.holeKind.toLowerCase();
|
||||
if (kind.includes('circle')) {
|
||||
return 'rounded-full';
|
||||
}
|
||||
if (kind.includes('triangle')) {
|
||||
return 'square-hole-runtime__hole-cut--triangle';
|
||||
}
|
||||
if (kind.includes('diamond')) {
|
||||
return 'rotate-45 rounded-[0.75rem]';
|
||||
}
|
||||
if (kind.includes('star')) {
|
||||
return 'square-hole-runtime__hole-cut--star';
|
||||
}
|
||||
if (kind.includes('arch')) {
|
||||
return 'rounded-t-full rounded-b-[0.85rem]';
|
||||
}
|
||||
return 'rounded-[0.85rem]';
|
||||
function clampPercent(value: number) {
|
||||
return Math.min(92, Math.max(8, value * 100));
|
||||
}
|
||||
|
||||
function getShapePreviewClass(shapeKind: string) {
|
||||
const kind = shapeKind.toLowerCase();
|
||||
if (kind.includes('circle')) {
|
||||
return 'rounded-full';
|
||||
function hashText(value: string) {
|
||||
let hash = 2166136261;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
if (kind.includes('triangle')) {
|
||||
return 'square-hole-runtime__shape--triangle';
|
||||
}
|
||||
if (kind.includes('diamond')) {
|
||||
return 'rotate-45 rounded-[0.8rem]';
|
||||
}
|
||||
if (kind.includes('star')) {
|
||||
return 'square-hole-runtime__shape--star';
|
||||
}
|
||||
if (kind.includes('arch')) {
|
||||
return 'rounded-t-full rounded-b-[0.9rem]';
|
||||
}
|
||||
return 'rounded-[0.9rem]';
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
function SquareHoleSettlement({
|
||||
@@ -166,9 +157,14 @@ export function SquareHoleRuntimeShell({
|
||||
const [pendingDrop, setPendingDrop] = useState<PendingDrop | null>(null);
|
||||
const [timeLeftMs, setTimeLeftMs] = useState(run?.remainingMs ?? 0);
|
||||
const [feedbackPulseId, setFeedbackPulseId] = useState<string | null>(null);
|
||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||
const [isShapeArmed, setIsShapeArmed] = useState(false);
|
||||
const [dropError, setDropError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeLeftMs(run?.remainingMs ?? 0);
|
||||
setIsShapeArmed(false);
|
||||
setDropError(null);
|
||||
}, [run?.remainingMs, run?.snapshotVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -205,6 +201,100 @@ export function SquareHoleRuntimeShell({
|
||||
return `${run.completedShapeCount}/${run.totalShapeCount}`;
|
||||
}, [run]);
|
||||
|
||||
const currentShape = run?.currentShape ?? null;
|
||||
const hintHole = useMemo(() => {
|
||||
if (!run || !currentShape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hintCandidates =
|
||||
run.holes.length > 1
|
||||
? run.holes.filter(
|
||||
(hole) => hole.holeId !== currentShape.targetHoleId,
|
||||
)
|
||||
: run.holes;
|
||||
if (hintCandidates.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const seed = `${run.runId}:${run.snapshotVersion}:${run.completedShapeCount}:${currentShape.shapeId}`;
|
||||
return hintCandidates[hashText(seed) % hintCandidates.length] ?? null;
|
||||
}, [currentShape, run]);
|
||||
|
||||
const arrowStyle = useMemo<CSSProperties>(() => {
|
||||
if (!hintHole) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
left: `${clampPercent(hintHole.x)}%`,
|
||||
top: `${clampPercent(hintHole.y)}%`,
|
||||
};
|
||||
}, [hintHole]);
|
||||
|
||||
const resolveHoleAtPoint = useCallback((clientX: number, clientY: number) => {
|
||||
const elements = document.elementsFromPoint(clientX, clientY);
|
||||
const holeElement = elements.find((element) =>
|
||||
element instanceof HTMLElement ? element.dataset.squareHoleId : false,
|
||||
) as HTMLElement | undefined;
|
||||
return holeElement?.dataset.squareHoleId ?? null;
|
||||
}, []);
|
||||
|
||||
const handleShapePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!run || !currentShape || !isRunning(run) || pendingDrop || isBusy) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
setDragState({
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
size: event.currentTarget.getBoundingClientRect().width,
|
||||
moved: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleShapePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!dragState || dragState.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
setDragState((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
moved:
|
||||
current.moved ||
|
||||
Math.hypot(
|
||||
event.clientX - current.startX,
|
||||
event.clientY - current.startY,
|
||||
) >= 6,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
};
|
||||
|
||||
const handleShapePointerEnd = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const currentDragState = dragState;
|
||||
if (!currentDragState || currentDragState.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||||
const holeId = resolveHoleAtPoint(event.clientX, event.clientY);
|
||||
setDragState(null);
|
||||
if (holeId) {
|
||||
void dropToHole(holeId);
|
||||
return;
|
||||
}
|
||||
if (!currentDragState.moved) {
|
||||
setIsShapeArmed(true);
|
||||
return;
|
||||
}
|
||||
setIsShapeArmed(false);
|
||||
};
|
||||
|
||||
const dropToHole = async (holeId: string) => {
|
||||
if (!run || !isRunning(run) || pendingDrop || isBusy) {
|
||||
return;
|
||||
@@ -213,6 +303,8 @@ export function SquareHoleRuntimeShell({
|
||||
const clientEventId = buildClientEventId(run.runId, holeId);
|
||||
setPendingDrop({ clientEventId, holeId });
|
||||
setFeedbackPulseId(null);
|
||||
setIsShapeArmed(false);
|
||||
setDropError(null);
|
||||
|
||||
try {
|
||||
const response = await onDropShape({
|
||||
@@ -224,6 +316,10 @@ export function SquareHoleRuntimeShell({
|
||||
});
|
||||
setFeedbackPulseId(clientEventId);
|
||||
onOptimisticRunChange?.(response.run);
|
||||
} catch (caughtError) {
|
||||
setDropError(
|
||||
caughtError instanceof Error ? caughtError.message : '本次投入失败',
|
||||
);
|
||||
} finally {
|
||||
setPendingDrop(null);
|
||||
}
|
||||
@@ -237,7 +333,6 @@ export function SquareHoleRuntimeShell({
|
||||
);
|
||||
}
|
||||
|
||||
const currentShape = run.currentShape;
|
||||
const feedback = run.lastFeedback;
|
||||
|
||||
return (
|
||||
@@ -253,6 +348,32 @@ export function SquareHoleRuntimeShell({
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(125,211,252,0.32),transparent_27%),radial-gradient(circle_at_80%_80%,rgba(248,113,113,0.24),transparent_34%),linear-gradient(180deg,#1f3a5f_0%,#152238_48%,#111827_100%)]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-slate-950/42" />
|
||||
{dragState && currentShape ? (
|
||||
<div
|
||||
className="pointer-events-none fixed z-[95] grid overflow-hidden rounded-[1.15rem] border border-white/18 bg-white/92 shadow-[0_24px_56px_rgba(15,23,42,0.46)]"
|
||||
style={{
|
||||
left: dragState.x,
|
||||
top: dragState.y,
|
||||
width: dragState.size,
|
||||
height: dragState.size,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{currentShape.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentShape.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-full w-full place-items-center bg-slate-100 text-slate-500">
|
||||
<Image size={30} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="relative flex min-h-dvh min-w-0 flex-col overflow-hidden px-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.8rem)] pt-[calc(env(safe-area-inset-top,0px)+0.65rem)]"
|
||||
style={{
|
||||
@@ -296,66 +417,53 @@ export function SquareHoleRuntimeShell({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-3 rounded-[1.5rem] border border-white/14 bg-black/18 p-3 shadow-[0_18px_42px_rgba(15,23,42,0.28)] backdrop-blur">
|
||||
<div className="flex items-center justify-between gap-2 text-xs font-bold text-white/68">
|
||||
<span>当前形状</span>
|
||||
<span>{progressText}</span>
|
||||
</div>
|
||||
<div className="mt-3 flex min-h-[12rem] items-center justify-center rounded-[1.35rem] border border-white/10 bg-white/10">
|
||||
{currentShape ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div
|
||||
className={`relative h-24 w-24 overflow-hidden shadow-[0_18px_38px_rgba(15,23,42,0.34)] ${getShapePreviewClass(
|
||||
currentShape.shapeKind,
|
||||
)}`}
|
||||
style={{
|
||||
background:
|
||||
currentShape.color ||
|
||||
'linear-gradient(135deg,#f8fafc,#38bdf8)',
|
||||
}}
|
||||
>
|
||||
{currentShape.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentShape.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-base font-black">
|
||||
<Shapes size={18} />
|
||||
<span>{currentShape.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm font-bold text-white/70">等待下一块</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-3 grid grid-cols-2 gap-2">
|
||||
<section className="relative mt-3 min-h-[22rem] overflow-hidden rounded-[1.5rem] border border-white/14 bg-black/18 p-3 shadow-[0_18px_42px_rgba(15,23,42,0.28)] backdrop-blur">
|
||||
<div className="absolute inset-3 rounded-[1.25rem] border border-white/10 bg-white/8" />
|
||||
{hintHole ? (
|
||||
<div
|
||||
className="square-hole-runtime__target-arrow pointer-events-none absolute z-20 -translate-x-1/2 -translate-y-[138%] text-cyan-100 drop-shadow-[0_4px_16px_rgba(103,232,249,0.65)]"
|
||||
style={arrowStyle}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ArrowDown size={34} strokeWidth={3.2} />
|
||||
</div>
|
||||
) : null}
|
||||
{run.holes.map((hole) => {
|
||||
const isPending = pendingDrop?.holeId === hole.holeId;
|
||||
const isHint = hintHole?.holeId === hole.holeId;
|
||||
return (
|
||||
<button
|
||||
key={hole.holeId}
|
||||
type="button"
|
||||
data-square-hole-id={hole.holeId}
|
||||
disabled={!isRunning(run) || Boolean(pendingDrop) || isBusy}
|
||||
className={`min-h-[6.25rem] rounded-[1.35rem] border border-white/14 bg-black/22 p-2 text-white shadow-[0_12px_28px_rgba(15,23,42,0.24)] backdrop-blur transition active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-58 ${
|
||||
isPending ? 'ring-2 ring-cyan-200/70' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
void dropToHole(hole.holeId);
|
||||
}}
|
||||
className={`absolute flex h-24 w-24 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center rounded-[1.35rem] border border-white/14 bg-black/34 p-2 text-white shadow-[0_12px_28px_rgba(15,23,42,0.24)] backdrop-blur transition disabled:cursor-not-allowed disabled:opacity-58 ${
|
||||
isPending || isHint || isShapeArmed
|
||||
? 'ring-2 ring-cyan-200/70'
|
||||
: ''
|
||||
}`}
|
||||
style={{
|
||||
left: `${clampPercent(hole.x)}%`,
|
||||
top: `${clampPercent(hole.y)}%`,
|
||||
}}
|
||||
aria-label={`投入 ${hole.label}`}
|
||||
>
|
||||
<span className="mx-auto grid h-12 w-12 place-items-center rounded-2xl bg-white/88 p-2">
|
||||
<span
|
||||
className={`block h-full w-full bg-slate-950 ${getHoleShapeClass(
|
||||
hole,
|
||||
)}`}
|
||||
/>
|
||||
<span className="grid h-14 w-14 place-items-center overflow-hidden rounded-2xl bg-white/88">
|
||||
{hole.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={hole.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-full w-full place-items-center bg-slate-100 text-slate-500">
|
||||
<Image size={22} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-2 block truncate text-sm font-black">
|
||||
{hole.label}
|
||||
@@ -365,6 +473,56 @@ export function SquareHoleRuntimeShell({
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="relative mt-3 min-h-[9.4rem] rounded-[1.45rem] border border-white/14 bg-black/24 p-3 backdrop-blur">
|
||||
{currentShape ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||
<div
|
||||
className={`relative h-24 w-24 touch-none select-none overflow-hidden shadow-[0_18px_38px_rgba(15,23,42,0.34)] transition ${
|
||||
dragState
|
||||
? 'opacity-35'
|
||||
: isShapeArmed
|
||||
? 'ring-4 ring-cyan-200/70 active:scale-[0.98]'
|
||||
: 'active:scale-[0.98]'
|
||||
} rounded-[1.15rem] border border-white/18 bg-white/92`}
|
||||
style={{
|
||||
cursor:
|
||||
isRunning(run) && !pendingDrop && !isBusy
|
||||
? 'grab'
|
||||
: 'default',
|
||||
}}
|
||||
onPointerDown={handleShapePointerDown}
|
||||
onPointerMove={handleShapePointerMove}
|
||||
onPointerUp={handleShapePointerEnd}
|
||||
onPointerCancel={handleShapePointerEnd}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`拖拽${currentShape.label}`}
|
||||
>
|
||||
{currentShape.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentShape.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-full w-full place-items-center bg-slate-100 text-slate-500">
|
||||
<Image size={32} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 text-base font-black">
|
||||
<Shapes size={18} />
|
||||
<span className="truncate">{currentShape.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm font-bold text-white/70">
|
||||
等待下一块
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mt-auto min-h-[3.5rem] pt-3">
|
||||
{feedback ? (
|
||||
<div
|
||||
@@ -376,9 +534,9 @@ export function SquareHoleRuntimeShell({
|
||||
>
|
||||
{feedback.message}
|
||||
</div>
|
||||
) : error ? (
|
||||
) : dropError || error ? (
|
||||
<div className="rounded-[1.2rem] border border-rose-200/35 bg-rose-400/18 px-3 py-2 text-center text-sm font-black text-rose-50">
|
||||
{error}
|
||||
{dropError ?? error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -1528,25 +1528,19 @@ body {
|
||||
opacity: 0.52;
|
||||
}
|
||||
|
||||
.square-hole-runtime__shape--triangle,
|
||||
.square-hole-runtime__hole-cut--triangle {
|
||||
clip-path: polygon(50% 5%, 94% 92%, 6% 92%);
|
||||
.square-hole-runtime__target-arrow {
|
||||
animation: square-hole-runtime-target-arrow 0.86s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.square-hole-runtime__shape--star,
|
||||
.square-hole-runtime__hole-cut--star {
|
||||
clip-path: polygon(
|
||||
50% 4%,
|
||||
61% 36%,
|
||||
95% 36%,
|
||||
67% 55%,
|
||||
79% 88%,
|
||||
50% 68%,
|
||||
21% 88%,
|
||||
33% 55%,
|
||||
5% 36%,
|
||||
39% 36%
|
||||
);
|
||||
@keyframes square-hole-runtime-target-arrow {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -138%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, -112%);
|
||||
}
|
||||
}
|
||||
|
||||
.platform-tab {
|
||||
|
||||
@@ -95,7 +95,7 @@ const SQUARE_HOLE_STEPS = [
|
||||
{
|
||||
id: 'square-hole-draft',
|
||||
label: '整理玩法草稿',
|
||||
detail: '收拢题材、形状、洞口与加分选项。',
|
||||
detail: '收拢题材、展示选项与洞口选项。',
|
||||
weight: 28,
|
||||
},
|
||||
{
|
||||
@@ -106,8 +106,8 @@ const SQUARE_HOLE_STEPS = [
|
||||
},
|
||||
{
|
||||
id: 'square-hole-shapes',
|
||||
label: '生成形状贴图',
|
||||
detail: '为每个可投放形状生成贴图。',
|
||||
label: '生成选项贴图',
|
||||
detail: '为展示选项与洞口选项生成贴图。',
|
||||
weight: 40,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
@@ -225,7 +225,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
...state,
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
: state;
|
||||
|
||||
const steps = getStepDefinitions(normalizedState.kind);
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
@@ -248,7 +248,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: 0;
|
||||
: 0;
|
||||
const overallProgress =
|
||||
normalizedState.phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
@@ -283,7 +283,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? Math.max(0, 12_000 - elapsedMs)
|
||||
: null,
|
||||
: null,
|
||||
activeStepIndex,
|
||||
steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState),
|
||||
};
|
||||
@@ -396,7 +396,8 @@ export function buildSquareHoleGenerationAnchorEntries(
|
||||
{
|
||||
key: 'square-hole-options',
|
||||
label: '选项资产',
|
||||
value: totalShapeCount > 0 ? `形状贴图 ${shapeCount}/${totalShapeCount}` : '',
|
||||
value:
|
||||
totalShapeCount > 0 ? `形状贴图 ${shapeCount}/${totalShapeCount}` : '',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export {
|
||||
listSquareHoleHistoryAssets,
|
||||
squareHoleAssetClient,
|
||||
type SquareHoleHistoryAsset,
|
||||
type SquareHoleImageAssetKind,
|
||||
} from './squareHoleAssetClient';
|
||||
export {
|
||||
deleteSquareHoleWork,
|
||||
getSquareHoleWorkDetail,
|
||||
listSquareHoleGallery,
|
||||
listSquareHoleWorks,
|
||||
publishSquareHoleWork,
|
||||
regenerateSquareHoleWorkImage,
|
||||
squareHoleWorksClient,
|
||||
updateSquareHoleWork,
|
||||
} from './squareHoleWorksClient';
|
||||
|
||||
46
src/services/square-hole-works/squareHoleAssetClient.ts
Normal file
46
src/services/square-hole-works/squareHoleAssetClient.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ASSET_API_PATHS } from '../../editor/shared/editorApiClient';
|
||||
import { requestJson } from '../apiClient';
|
||||
|
||||
export type SquareHoleImageAssetKind =
|
||||
| 'square_hole_cover_image'
|
||||
| 'square_hole_background_image'
|
||||
| 'square_hole_shape_image'
|
||||
| 'square_hole_hole_image';
|
||||
|
||||
export type SquareHoleHistoryAsset = {
|
||||
assetObjectId: string;
|
||||
assetKind: SquareHoleImageAssetKind;
|
||||
imageSrc: string;
|
||||
ownerUserId?: string | null;
|
||||
ownerLabel: string;
|
||||
profileId?: string | null;
|
||||
entityId?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取当前账号的方洞图片历史素材。
|
||||
* 素材由后端图片生成链路写入正式资产索引,前端只负责按槽位展示和套用。
|
||||
*/
|
||||
export async function listSquareHoleHistoryAssets(payload: {
|
||||
kind: SquareHoleImageAssetKind;
|
||||
limit?: number;
|
||||
}) {
|
||||
const params = new URLSearchParams({ kind: payload.kind });
|
||||
if (payload.limit) {
|
||||
params.set('limit', String(payload.limit));
|
||||
}
|
||||
|
||||
const response = await requestJson<{ assets: SquareHoleHistoryAsset[] }>(
|
||||
`${ASSET_API_PATHS.assetHistory}?${params.toString()}`,
|
||||
{ method: 'GET' },
|
||||
'读取方洞历史图片失败',
|
||||
);
|
||||
|
||||
return response.assets;
|
||||
}
|
||||
|
||||
export const squareHoleAssetClient = {
|
||||
listHistoryAssets: listSquareHoleHistoryAssets,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
PutSquareHoleWorkRequest,
|
||||
RegenerateSquareHoleWorkImageRequest,
|
||||
SquareHoleWorkDetailResponse,
|
||||
SquareHoleWorkMutationResponse,
|
||||
SquareHoleWorksResponse,
|
||||
@@ -91,6 +92,25 @@ export function publishSquareHoleWork(profileId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只重生成某一个方洞挑战图片槽位,不触发 Agent 草稿编译或整稿图片生成进度页。
|
||||
*/
|
||||
export function regenerateSquareHoleWorkImage(
|
||||
profileId: string,
|
||||
payload: RegenerateSquareHoleWorkImageRequest,
|
||||
) {
|
||||
return requestJson<SquareHoleWorkMutationResponse>(
|
||||
`${SQUARE_HOLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}/images/regenerate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'生成方洞挑战图片失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前用户的方洞挑战作品,并返回删除后的列表。
|
||||
*/
|
||||
@@ -109,5 +129,6 @@ export const squareHoleWorksClient = {
|
||||
listGallery: listSquareHoleGallery,
|
||||
list: listSquareHoleWorks,
|
||||
publish: publishSquareHoleWork,
|
||||
regenerateImage: regenerateSquareHoleWorkImage,
|
||||
update: updateSquareHoleWork,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user