1
This commit is contained in:
@@ -41,6 +41,7 @@ type PuzzleRuntimeShellProps = {
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onBack: () => void;
|
||||
onRemodelWork?: (profileId: string) => void | Promise<void>;
|
||||
onSwapPieces: (payload: SwapPuzzlePiecesRequest) => void;
|
||||
onDragPiece: (payload: DragPuzzlePieceRequest) => void;
|
||||
onAdvanceNextLevel: (target?: PuzzleNextLevelTarget) => void;
|
||||
@@ -208,6 +209,61 @@ const PUZZLE_CLEAR_DIALOG_DELAY_MS = 500;
|
||||
const PUZZLE_MERGE_FLASH_DURATION_MS = 720;
|
||||
const PUZZLE_HINT_DEMO_DURATION_MS = 1_250;
|
||||
const PUZZLE_PIECE_PRESS_HAPTIC_PATTERN_MS = 12;
|
||||
const PUZZLE_EXIT_REMODEL_PROMPT_STORAGE_PREFIX =
|
||||
'genarrative.puzzle-runtime.exit-remodel-prompt.v1';
|
||||
|
||||
const shownExitRemodelPromptProfileIds = new Set<string>();
|
||||
|
||||
function buildExitRemodelPromptStorageKey(profileId: string) {
|
||||
return `${PUZZLE_EXIT_REMODEL_PROMPT_STORAGE_PREFIX}:${encodeURIComponent(
|
||||
profileId,
|
||||
)}`;
|
||||
}
|
||||
|
||||
function hasSeenExitRemodelPrompt(profileId: string) {
|
||||
const normalizedProfileId = profileId.trim();
|
||||
if (!normalizedProfileId) {
|
||||
return true;
|
||||
}
|
||||
if (shownExitRemodelPromptProfileIds.has(normalizedProfileId)) {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const seen =
|
||||
window.localStorage.getItem(
|
||||
buildExitRemodelPromptStorageKey(normalizedProfileId),
|
||||
) === '1';
|
||||
if (seen) {
|
||||
shownExitRemodelPromptProfileIds.add(normalizedProfileId);
|
||||
}
|
||||
return seen;
|
||||
} catch {
|
||||
return shownExitRemodelPromptProfileIds.has(normalizedProfileId);
|
||||
}
|
||||
}
|
||||
|
||||
function markExitRemodelPromptSeen(profileId: string) {
|
||||
const normalizedProfileId = profileId.trim();
|
||||
if (!normalizedProfileId) {
|
||||
return;
|
||||
}
|
||||
shownExitRemodelPromptProfileIds.add(normalizedProfileId);
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
buildExitRemodelPromptStorageKey(normalizedProfileId),
|
||||
'1',
|
||||
);
|
||||
} catch {
|
||||
// 中文注释:隐私模式下 localStorage 可能不可写,内存集合足够兜底本次挂载周期。
|
||||
}
|
||||
}
|
||||
|
||||
type PuzzlePropDialogState = {
|
||||
propKind: PuzzleRuntimePropKind;
|
||||
@@ -251,6 +307,7 @@ export function PuzzleRuntimeShell({
|
||||
isBusy = false,
|
||||
error = null,
|
||||
onBack,
|
||||
onRemodelWork,
|
||||
onSwapPieces,
|
||||
onDragPiece,
|
||||
onAdvanceNextLevel,
|
||||
@@ -263,6 +320,8 @@ export function PuzzleRuntimeShell({
|
||||
const authUi = useAuthUi();
|
||||
const [selectedPieceId, setSelectedPieceId] = useState<string | null>(null);
|
||||
const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false);
|
||||
const [isExitRemodelPromptOpen, setIsExitRemodelPromptOpen] =
|
||||
useState(false);
|
||||
const [propDialog, setPropDialog] = useState<PuzzlePropDialogState | null>(
|
||||
null,
|
||||
);
|
||||
@@ -621,7 +680,10 @@ export function PuzzleRuntimeShell({
|
||||
}, [onTimeExpired]);
|
||||
|
||||
const isUiPauseActive =
|
||||
isSettingsPanelOpen || Boolean(propDialog) || isOriginalOverlayVisible;
|
||||
isSettingsPanelOpen ||
|
||||
isExitRemodelPromptOpen ||
|
||||
Boolean(propDialog) ||
|
||||
isOriginalOverlayVisible;
|
||||
|
||||
useEffect(() => {
|
||||
if (previousUiPauseActiveRef.current === isUiPauseActive) {
|
||||
@@ -898,6 +960,7 @@ export function PuzzleRuntimeShell({
|
||||
const authorAvatarLabel = resolveAuthorAvatarLabel(
|
||||
currentLevel.authorDisplayName,
|
||||
);
|
||||
const exitPromptProfileId = currentLevel.profileId.trim();
|
||||
const leaderboardEntries =
|
||||
(currentLevel.leaderboardEntries ?? []).length > 0
|
||||
? currentLevel.leaderboardEntries
|
||||
@@ -909,6 +972,20 @@ export function PuzzleRuntimeShell({
|
||||
const isInteractionLocked =
|
||||
isBusy || runtimeStatus !== 'playing' || Boolean(propDialog);
|
||||
|
||||
const handleBackRequest = () => {
|
||||
if (
|
||||
onRemodelWork &&
|
||||
exitPromptProfileId &&
|
||||
!hasSeenExitRemodelPrompt(exitPromptProfileId)
|
||||
) {
|
||||
markExitRemodelPromptSeen(exitPromptProfileId);
|
||||
setIsExitRemodelPromptOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onBack();
|
||||
};
|
||||
|
||||
const openPropDialog = (propKind: PuzzleRuntimePropKind, title: string) => {
|
||||
const canOpen =
|
||||
propKind === 'extendTime'
|
||||
@@ -1016,7 +1093,7 @@ export function PuzzleRuntimeShell({
|
||||
<div className="grid grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] items-start gap-2 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
onClick={handleBackRequest}
|
||||
aria-label="返回上一页"
|
||||
className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur"
|
||||
>
|
||||
@@ -1664,6 +1741,54 @@ export function PuzzleRuntimeShell({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isExitRemodelPromptOpen ? (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex items-center justify-center bg-slate-950/72 px-4 py-6 backdrop-blur-sm"
|
||||
>
|
||||
<section
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="puzzle-exit-remodel-title"
|
||||
className="flex w-full max-w-[22rem] flex-col overflow-hidden rounded-[1.5rem] border border-white/14 bg-slate-950/94 shadow-[0_28px_90px_rgba(0,0,0,0.5)]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="px-5 pt-6 text-center">
|
||||
<h2
|
||||
id="puzzle-exit-remodel-title"
|
||||
className="text-2xl font-black leading-tight text-white"
|
||||
>
|
||||
体验不佳?
|
||||
<br />
|
||||
试试改造功能!
|
||||
</h2>
|
||||
</header>
|
||||
<footer className="grid gap-3 px-5 py-5">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setIsExitRemodelPromptOpen(false);
|
||||
void onRemodelWork?.(exitPromptProfileId);
|
||||
}}
|
||||
className="rounded-full bg-amber-200 px-5 py-3 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
作品改造
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsExitRemodelPromptOpen(false);
|
||||
onBack();
|
||||
}}
|
||||
className="rounded-full border border-white/14 bg-black/24 px-5 py-3 text-sm font-black text-white transition hover:bg-white/10"
|
||||
>
|
||||
保存并退出
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{runtimeStatus === 'failed' ? (
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center bg-slate-950/68 px-4 py-6 backdrop-blur-sm">
|
||||
<section
|
||||
|
||||
Reference in New Issue
Block a user