1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-29 23:10:43 +08:00
parent 0395bd7ec6
commit 89ab1bf1c0
20 changed files with 204 additions and 244 deletions

View File

@@ -99,11 +99,9 @@ import {
} from '../../services/puzzle-gallery';
import {
advanceLocalPuzzleNextLevel,
dragPuzzlePieceOrGroup,
getPuzzleRun,
startPuzzleRun,
submitPuzzleLeaderboard,
swapPuzzlePieces,
updatePuzzleRunPause,
usePuzzleRuntimeProp as consumePuzzleRuntimeProp,
} from '../../services/puzzle-runtime';
@@ -557,6 +555,37 @@ function LazyPanelFallback({ label }: { label: string }) {
);
}
function mergePuzzleServiceRuntimeState(
currentRun: PuzzleRunSnapshot,
serviceRun: PuzzleRunSnapshot,
): PuzzleRunSnapshot {
if (!currentRun.currentLevel || !serviceRun.currentLevel) {
return currentRun;
}
const serviceLevel = serviceRun.currentLevel;
const leaderboardEntries =
serviceLevel.leaderboardEntries.length > 0
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
return {
...currentRun,
leaderboardEntries,
currentLevel: {
...currentRun.currentLevel,
timeLimitMs: serviceLevel.timeLimitMs,
remainingMs: serviceLevel.remainingMs,
pausedAccumulatedMs: serviceLevel.pausedAccumulatedMs,
pauseStartedAtMs: serviceLevel.pauseStartedAtMs,
freezeAccumulatedMs: serviceLevel.freezeAccumulatedMs,
freezeStartedAtMs: serviceLevel.freezeStartedAtMs,
freezeUntilMs: serviceLevel.freezeUntilMs,
leaderboardEntries,
},
};
}
export function PlatformEntryFlowShellImpl({
selectionStage,
setSelectionStage,
@@ -1565,20 +1594,10 @@ export function PlatformEntryFlowShellImpl({
}
setPuzzleError(null);
if (isLocalPuzzleRun(puzzleRun)) {
setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload));
return;
}
void swapPuzzlePieces(puzzleRun.runId, payload)
.then(({ run }) => {
setPuzzleRun(run);
})
.catch((error) => {
setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。'));
});
// 交换、合并与通关判定都由前端即时裁决,正式 run 不再等待后端 /swap。
setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage, setPuzzleError],
[isPuzzleBusy, puzzleRun, setPuzzleError],
);
const dragPuzzlePiece = useCallback(
@@ -1588,20 +1607,11 @@ export function PlatformEntryFlowShellImpl({
}
setPuzzleError(null);
if (isLocalPuzzleRun(puzzleRun)) {
setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload));
return;
}
void dragPuzzlePieceOrGroup(puzzleRun.runId, payload)
.then(({ run }) => {
setPuzzleRun(run);
})
.catch((error) => {
setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。'));
});
// 拖动落点、合并、拆分与通关判定都属于前端即时交互裁决。
// 后端只保留开局、道具、下一关与真实排行榜等服务侧能力。
setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage, setPuzzleError],
[isPuzzleBusy, puzzleRun, setPuzzleError],
);
useEffect(() => {
@@ -1641,7 +1651,9 @@ export function PlatformEntryFlowShellImpl({
const { run } = await updatePuzzleRunPause(puzzleRun.runId, {
paused,
});
setPuzzleRun(run);
setPuzzleRun((currentRun) =>
currentRun ? mergePuzzleServiceRuntimeState(currentRun, run) : currentRun,
);
void platformBootstrap.refreshProfileDashboard();
} catch (error) {
setPuzzleError(
@@ -1669,7 +1681,9 @@ export function PlatformEntryFlowShellImpl({
try {
const { run } = await getPuzzleRun(puzzleRun.runId);
setPuzzleRun(run);
setPuzzleRun((currentRun) =>
currentRun ? mergePuzzleServiceRuntimeState(currentRun, run) : currentRun,
);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '同步拼图失败状态失败。'),
@@ -1703,9 +1717,14 @@ export function PlatformEntryFlowShellImpl({
const { run } = await consumePuzzleRuntimeProp(puzzleRun.runId, {
propKind,
});
setPuzzleRun(run);
const nextRun = mergePuzzleServiceRuntimeState(
puzzleRunRef.current ?? puzzleRun,
run,
);
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
void platformBootstrap.refreshProfileDashboard();
return run;
return nextRun;
},
[platformBootstrap, puzzleRun],
);
@@ -1744,7 +1763,12 @@ export function PlatformEntryFlowShellImpl({
void submitPuzzleLeaderboard(puzzleRun.runId, payload)
.then(({ run }) => {
setPuzzleRun(run);
setPuzzleRun((currentRun) => {
if (!currentRun) {
return currentRun;
}
return mergePuzzleServiceRuntimeState(currentRun, run);
});
})
.catch((error) => {
submittedPuzzleLeaderboardKeysRef.current.delete(submitKey);

View File

@@ -427,7 +427,7 @@ function PuzzleHistoryAssetPickerDialog({
onClick={() => onSelect(asset)}
className={`overflow-hidden rounded-[1.35rem] border bg-white/82 text-left transition hover:border-amber-300/70 hover:bg-white ${isBusy ? 'cursor-not-allowed opacity-55' : 'border-[var(--platform-subpanel-border)]'}`}
>
<div className="aspect-[9/16] overflow-hidden bg-[var(--platform-subpanel-fill)]">
<div className="aspect-square overflow-hidden bg-[var(--platform-subpanel-fill)]">
<ResolvedAssetImage
src={asset.imageSrc}
alt={asset.ownerLabel || '历史拼图素材'}
@@ -509,7 +509,7 @@ function PuzzlePictureEditor({
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
<div className="mx-auto mt-3 aspect-[9/16] w-full max-w-[24rem] overflow-hidden rounded-[1.25rem] bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.14),transparent_34%),linear-gradient(145deg,rgba(76,29,19,0.86),rgba(30,41,59,0.94))]">
<div className="mx-auto mt-3 aspect-square w-full max-w-[24rem] overflow-hidden rounded-[1.25rem] bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.14),transparent_34%),linear-gradient(145deg,rgba(76,29,19,0.86),rgba(30,41,59,0.94))]">
{formalImageSrc ? (
<ResolvedAssetImage
src={formalImageSrc}
@@ -725,7 +725,7 @@ function PuzzlePublishDialog({
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
<div className="aspect-[9/16] overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
<div className="aspect-square overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
{formalImageSrc ? (
<ResolvedAssetImage
src={formalImageSrc}

View File

@@ -206,7 +206,7 @@ test('右上角设置按钮打开拼图设置并支持音量调节', () => {
expect(authValue.setMusicVolume).toHaveBeenCalledWith(0.77);
});
test('拼图棋盘使用 9:16 竖屏舞台承载切块', () => {
test('拼图棋盘使用贴近移动端边缘的正方形舞台承载切块', () => {
const { container } = renderPuzzleRuntime(
<PuzzleRuntimeShell
run={{
@@ -229,9 +229,10 @@ test('拼图棋盘使用 9:16 竖屏舞台承载切块', () => {
);
const board = screen.getByTestId('puzzle-board');
expect(board.className).toContain('aspect-[9/16]');
expect(board.className).toContain('aspect-square');
expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_16.5rem))]');
expect(board.className).not.toContain('aspect-video');
expect(board.className).not.toContain('aspect-square');
expect(board.className).not.toContain('aspect-[9/16]');
expect(board.getAttribute('style')).toContain('grid-template-rows');
expect(container.querySelector('.min-h-\\[4\\.5rem\\]')).toBeNull();
});

View File

@@ -331,7 +331,8 @@ type PuzzleHintDemoState = {
/**
* 拼图运行时壳层。
* 前端维护轻量选中态与拖拽目标,交换、合并、拆分与通关全部以后端快照为准
* 前端维护运行时即时交互:交换、拖动、合并、拆分与本关通关在前端裁决
* 后端继续负责开始关卡、下一关候选、道具扣费、排行榜等服务侧能力。
*/
export function PuzzleRuntimeShell({
run,
@@ -1110,11 +1111,11 @@ export function PuzzleRuntimeShell({
</div>
</div>
<div className="absolute inset-0 flex items-center justify-center p-3 pt-28 pb-32 sm:p-4">
<div className="absolute inset-0 flex items-center justify-center px-1 py-3 pt-28 pb-32 sm:p-4">
<div
ref={boardRef}
data-testid="puzzle-board"
className="relative grid aspect-[9/16] w-full max-w-[min(96vw,calc(56.25vh_-_8.5rem))] touch-none select-none overflow-hidden rounded-[1.2rem] border border-white/16 bg-white/8 shadow-[0_30px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm sm:rounded-[1.45rem]"
className="relative grid aspect-square w-full max-w-[min(99vw,calc(100vh_-_16.5rem))] touch-none select-none overflow-hidden rounded-[1.2rem] border border-white/16 bg-white/8 shadow-[0_30px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm sm:max-w-[min(92vw,calc(100vh_-_17rem))] sm:rounded-[1.45rem]"
style={{
gridTemplateColumns: `repeat(${board.cols}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${board.rows}, minmax(0, 1fr))`,

View File

@@ -138,7 +138,6 @@ vi.mock('../../services/puzzle-gallery', () => ({
vi.mock('../../services/puzzle-runtime', () => ({
advanceLocalPuzzleNextLevel: vi.fn(),
dragPuzzlePieceOrGroup: vi.fn(),
startPuzzleRun: vi.fn(),
swapPuzzlePieces: vi.fn(),
submitPuzzleLeaderboard: vi.fn(),