继续收口账号与运行态弹窗壳层

账号设置弹窗复用认证壳层并保留 direct mode 唯一 dialog 语义

拼图运行态新增本地弹窗壳层收口确认设置退出失败和通关结算

抓大鹅与跳一跳结算弹窗提取本地结算结构并补测试

拼图 onboarding 登录保存覆盖层迁入 UnifiedModal

更新 PlatformUiKit 收口文档和 Hermes 决策记录
This commit is contained in:
2026-06-11 21:53:10 +08:00
parent 59facaf14b
commit ed2c386603
14 changed files with 821 additions and 394 deletions

View File

@@ -58,6 +58,11 @@ import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast';
import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import {
PuzzleRuntimeDialogButton,
PuzzleRuntimeDialogFooter,
PuzzleRuntimeModalShell,
} from './PuzzleRuntimeModalShell';
import {
buildMergedGroupOutlinePath,
buildRoundedGridCellClipPath,
@@ -1372,13 +1377,11 @@ export function PuzzleRuntimeShell({
};
const clearResultDialog = isClearResultOpen ? (
<div className={clearResultOverlayClassName}>
<section
role="dialog"
aria-modal="true"
aria-labelledby="puzzle-clear-result-title"
className="puzzle-runtime-dialog flex max-h-[min(92vh,42rem)] w-full max-w-[28rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
>
<PuzzleRuntimeModalShell
titleId="puzzle-clear-result-title"
overlayClassName={clearResultOverlayClassName}
dialogClassName="puzzle-runtime-dialog flex max-h-[min(92vh,42rem)] w-full max-w-[28rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
>
<header className="puzzle-runtime-dialog__line flex items-start justify-between gap-3 border-b px-5 py-4">
<div className="min-w-0">
<div className="puzzle-runtime-primary-button mb-2 inline-flex h-9 w-9 items-center justify-center rounded-full">
@@ -1394,16 +1397,17 @@ export function PuzzleRuntimeShell({
{currentLevel.levelName}
</div>
</div>
<button
<PuzzleRuntimeDialogButton
type="button"
tone="secondary"
aria-label="关闭通关弹窗"
className="puzzle-runtime-secondary-button inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition hover:brightness-105"
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition hover:brightness-105"
onClick={() => {
setDismissedClearKey(clearResultKey);
}}
>
<X className="h-4 w-4" />
</button>
</PuzzleRuntimeDialogButton>
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
@@ -1493,7 +1497,7 @@ export function PuzzleRuntimeShell({
</div>
{canAdvanceDefaultNextLevel ? (
<footer className="puzzle-runtime-dialog__line flex items-center justify-end border-t px-5 py-4">
<PuzzleRuntimeDialogFooter className="flex items-center justify-end px-5 py-4">
<button
type="button"
aria-label="下一关"
@@ -1523,10 +1527,9 @@ export function PuzzleRuntimeShell({
</>
)}
</button>
</footer>
</PuzzleRuntimeDialogFooter>
) : null}
</section>
</div>
</PuzzleRuntimeModalShell>
) : null;
const clearResultLayer =
embedded && clearResultDialog && typeof document !== 'undefined'
@@ -2174,290 +2177,261 @@ export function PuzzleRuntimeShell({
) : null}
{propDialog ? (
<div
className="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm"
onClick={() => {
<PuzzleRuntimeModalShell
titleId="puzzle-prop-confirm-title"
onOverlayClick={() => {
if (!isPropConfirming) {
setPropDialog(null);
}
}}
>
<section
role="dialog"
aria-modal="true"
aria-labelledby="puzzle-prop-confirm-title"
className="puzzle-runtime-dialog w-full max-w-[22rem] overflow-hidden rounded-[1.35rem] shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
onClick={(event) => event.stopPropagation()}
>
<header className="puzzle-runtime-dialog__line flex items-center gap-3 border-b px-5 py-4">
<span className="puzzle-runtime-primary-button inline-flex h-9 w-9 items-center justify-center rounded-full">
<Sparkles className="h-4 w-4" />
</span>
<h2
id="puzzle-prop-confirm-title"
className="text-sm font-black"
<header className="puzzle-runtime-dialog__line flex items-center gap-3 border-b px-5 py-4">
<span className="puzzle-runtime-primary-button inline-flex h-9 w-9 items-center justify-center rounded-full">
<Sparkles className="h-4 w-4" />
</span>
<h2
id="puzzle-prop-confirm-title"
className="text-sm font-black"
>
{propDialog.title}
</h2>
</header>
<div className="puzzle-runtime-dialog__body px-5 py-4 text-sm">
消耗 1 泥点
{propConfirmError ? (
<PlatformRuntimeStatusToast
tone="error"
size="xs"
shape="rounded"
className="mt-3"
>
{propDialog.title}
</h2>
</header>
<div className="puzzle-runtime-dialog__body px-5 py-4 text-sm">
消耗 1 泥点
{propConfirmError ? (
<PlatformRuntimeStatusToast
tone="error"
size="xs"
shape="rounded"
className="mt-3"
>
{propConfirmError}
</PlatformRuntimeStatusToast>
{propConfirmError}
</PlatformRuntimeStatusToast>
) : null}
</div>
<PuzzleRuntimeDialogFooter className="flex items-center justify-end gap-3 px-5 py-4">
<PuzzleRuntimeDialogButton
tone="secondary"
onClick={() => setPropDialog(null)}
disabled={isPropConfirming}
className="rounded-full px-4 py-2 text-xs font-bold transition hover:brightness-105"
>
取消
</PuzzleRuntimeDialogButton>
<PuzzleRuntimeDialogButton
tone="primary"
disabled={isPropConfirming}
onClick={() => {
void handleConfirmProp();
}}
className="inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-black transition hover:brightness-105 disabled:opacity-60"
>
{isPropConfirming ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : null}
</div>
<footer className="puzzle-runtime-dialog__line flex items-center justify-end gap-3 border-t px-5 py-4">
<button
type="button"
onClick={() => setPropDialog(null)}
disabled={isPropConfirming}
className="puzzle-runtime-secondary-button rounded-full px-4 py-2 text-xs font-bold transition hover:brightness-105"
>
取消
</button>
<button
type="button"
disabled={isPropConfirming}
onClick={() => {
void handleConfirmProp();
}}
className="puzzle-runtime-primary-button inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-black transition hover:brightness-105 disabled:opacity-60"
>
{isPropConfirming ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : null}
确定
</button>
</footer>
</section>
</div>
确定
</PuzzleRuntimeDialogButton>
</PuzzleRuntimeDialogFooter>
</PuzzleRuntimeModalShell>
) : null}
{isSettingsPanelOpen ? (
<div
className="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center p-3 backdrop-blur-sm sm:p-4"
onClick={() => setIsSettingsPanelOpen(false)}
<PuzzleRuntimeModalShell
titleId="puzzle-settings-title"
overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center p-3 backdrop-blur-sm sm:p-4"
dialogClassName="puzzle-runtime-dialog flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
onOverlayClick={() => setIsSettingsPanelOpen(false)}
>
<section
role="dialog"
aria-modal="true"
aria-labelledby="puzzle-settings-title"
className="puzzle-runtime-dialog flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
onClick={(event) => event.stopPropagation()}
>
<header className="puzzle-runtime-dialog__line relative border-b px-4 py-3 sm:px-5 sm:py-4">
<div className="min-w-0 pr-10">
<h2
id="puzzle-settings-title"
className="text-sm font-semibold"
>
拼图设置
</h2>
<div className="puzzle-runtime-dialog__soft mt-1 text-[11px]">
{hideExitControls
? '调整音乐音量,查看本局进度。'
: '调整音乐音量,查看本局进度,或返回上一页。'}
</div>
<header className="puzzle-runtime-dialog__line relative border-b px-4 py-3 sm:px-5 sm:py-4">
<div className="min-w-0 pr-10">
<h2 id="puzzle-settings-title" className="text-sm font-semibold">
拼图设置
</h2>
<div className="puzzle-runtime-dialog__soft mt-1 text-[11px]">
{hideExitControls
? '调整音乐音量,查看本局进度。'
: '调整音乐音量,查看本局进度,或返回上一页。'}
</div>
<button
type="button"
aria-label="关闭拼图设置"
onClick={() => setIsSettingsPanelOpen(false)}
className="puzzle-runtime-dialog__soft absolute right-4 top-3 p-1 transition-colors hover:brightness-75 sm:right-5 sm:top-4"
>
<X className="h-4 w-4" />
</button>
</header>
</div>
<button
type="button"
aria-label="关闭拼图设置"
onClick={() => setIsSettingsPanelOpen(false)}
className="puzzle-runtime-dialog__soft absolute right-4 top-3 p-1 transition-colors hover:brightness-75 sm:right-5 sm:top-4"
>
<X className="h-4 w-4" />
</button>
</header>
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
<div className="puzzle-runtime-settings-card rounded-2xl p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="puzzle-runtime-tool-button__cool text-[10px] tracking-[0.24em]">
音频
</div>
<div className="mt-2 text-sm font-semibold">音乐音量</div>
</div>
<div className="puzzle-runtime-pill rounded-full px-2 py-1 text-xs">
{Math.round(musicVolume * 100)}%
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
<div className="puzzle-runtime-settings-card rounded-2xl p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="puzzle-runtime-tool-button__cool text-[10px] tracking-[0.24em]">
音频
</div>
<div className="mt-2 text-sm font-semibold">音乐音量</div>
</div>
<div className="mt-4 flex items-center gap-3">
<input
type="range"
min={0}
max={100}
step={1}
aria-label="拼图音乐音量"
value={Math.round(musicVolume * 100)}
onChange={(event) =>
onMusicVolumeChange(
Number(event.currentTarget.value) / 100,
)
}
className="h-2 w-full cursor-pointer accent-[var(--puzzle-runtime-accent-text)]"
/>
<div className="puzzle-runtime-pill rounded-full px-2 py-1 text-xs">
{Math.round(musicVolume * 100)}%
</div>
</div>
<div className="puzzle-runtime-settings-card rounded-2xl px-4 py-3">
<div className="puzzle-runtime-dialog__soft text-[10px] uppercase tracking-[0.18em]">
本局进度
</div>
<div className="puzzle-runtime-dialog__body mt-3 space-y-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">关卡</span>
<span className="font-semibold">{levelLabel}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">
已完成关卡
</span>
<span className="font-semibold">
{run.clearedLevelCount}
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">
当前状态
</span>
<span className="font-semibold">{statusLabel}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">
当前用时
</span>
<span className="font-mono font-semibold">
{formatElapsedMs(displayElapsedMs)}
</span>
</div>
</div>
<div className="mt-4 flex items-center gap-3">
<input
type="range"
min={0}
max={100}
step={1}
aria-label="拼图音乐音量"
value={Math.round(musicVolume * 100)}
onChange={(event) =>
onMusicVolumeChange(Number(event.currentTarget.value) / 100)
}
className="h-2 w-full cursor-pointer accent-[var(--puzzle-runtime-accent-text)]"
/>
</div>
</div>
<footer className="puzzle-runtime-dialog__line flex items-center justify-end gap-3 border-t px-4 py-3 sm:px-5">
<button
type="button"
onClick={() => setIsSettingsPanelOpen(false)}
className="puzzle-runtime-secondary-button rounded-full px-3 py-1.5 text-[11px] transition hover:brightness-105"
<div className="puzzle-runtime-settings-card rounded-2xl px-4 py-3">
<div className="puzzle-runtime-dialog__soft text-[10px] uppercase tracking-[0.18em]">
本局进度
</div>
<div className="puzzle-runtime-dialog__body mt-3 space-y-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">关卡</span>
<span className="font-semibold">{levelLabel}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">
已完成关卡
</span>
<span className="font-semibold">{run.clearedLevelCount}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">当前状态</span>
<span className="font-semibold">{statusLabel}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="puzzle-runtime-dialog__soft">当前用时</span>
<span className="font-mono font-semibold">
{formatElapsedMs(displayElapsedMs)}
</span>
</div>
</div>
</div>
</div>
<PuzzleRuntimeDialogFooter className="flex items-center justify-end gap-3 px-4 py-3 sm:px-5">
<PuzzleRuntimeDialogButton
tone="secondary"
onClick={() => setIsSettingsPanelOpen(false)}
className="rounded-full px-3 py-1.5 text-[11px] transition hover:brightness-105"
>
继续拼图
</PuzzleRuntimeDialogButton>
{!hideExitControls ? (
<PuzzleRuntimeDialogButton
tone="primary"
onClick={() => {
setIsSettingsPanelOpen(false);
onBack();
}}
className={`rounded-full px-4 py-2 text-sm font-bold transition hover:brightness-105 ${
shouldHideBackButton ? 'hidden' : ''
}`}
>
继续拼图
</button>
{!hideExitControls ? (
<button
type="button"
onClick={() => {
setIsSettingsPanelOpen(false);
onBack();
}}
className={`puzzle-runtime-primary-button rounded-full px-4 py-2 text-sm font-bold transition hover:brightness-105 ${
shouldHideBackButton ? 'hidden' : ''
}`}
>
返回上一页
</button>
) : null}
</footer>
</section>
</div>
返回上一页
</PuzzleRuntimeDialogButton>
) : null}
</PuzzleRuntimeDialogFooter>
</PuzzleRuntimeModalShell>
) : null}
{isExitRemodelPromptOpen && !hideExitControls ? (
<div className="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center px-4 py-6 backdrop-blur-md">
<section
role="dialog"
aria-modal="true"
aria-labelledby="puzzle-exit-remodel-title"
className="puzzle-runtime-dialog relative flex w-full max-w-[21rem] flex-col overflow-hidden rounded-[1.35rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
onClick={(event) => event.stopPropagation()}
<PuzzleRuntimeModalShell
titleId="puzzle-exit-remodel-title"
overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center px-4 py-6 backdrop-blur-md"
dialogClassName="puzzle-runtime-dialog relative flex w-full max-w-[21rem] flex-col overflow-hidden rounded-[1.35rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
>
<div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-[var(--puzzle-runtime-accent-text)] to-transparent" />
<header className="flex flex-col items-center px-6 pt-7 text-center">
<div className="puzzle-runtime-stat-card mb-4 grid h-14 w-14 place-items-center rounded-2xl">
<Sparkles className="puzzle-runtime-tool-button__warm h-7 w-7" />
</div>
<h2
id="puzzle-exit-remodel-title"
className="text-[1.75rem] font-black leading-[1.08]"
>
体验不佳?
<br />
试试改造功能!
</h2>
</header>
<PuzzleRuntimeDialogFooter
className="grid gap-3 px-5 pb-5 pt-6"
framed={false}
>
<div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-[var(--puzzle-runtime-accent-text)] to-transparent" />
<header className="flex flex-col items-center px-6 pt-7 text-center">
<div className="puzzle-runtime-stat-card mb-4 grid h-14 w-14 place-items-center rounded-2xl">
<Sparkles className="puzzle-runtime-tool-button__warm h-7 w-7" />
</div>
<h2
id="puzzle-exit-remodel-title"
className="text-[1.75rem] font-black leading-[1.08]"
>
体验不佳?
<br />
试试改造功能!
</h2>
</header>
<footer className="grid gap-3 px-5 pb-5 pt-6">
<button
type="button"
disabled={isBusy}
onClick={() => {
setIsExitRemodelPromptOpen(false);
void onRemodelWork?.(exitPromptProfileId);
}}
className="puzzle-runtime-primary-button min-h-[3.25rem] rounded-2xl px-5 text-sm font-black transition hover:brightness-105 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50"
>
作品改造
</button>
<button
type="button"
onClick={() => {
setIsExitRemodelPromptOpen(false);
onBack();
}}
className="puzzle-runtime-secondary-button min-h-[3rem] rounded-2xl px-5 text-sm font-bold transition hover:brightness-105 active:translate-y-px"
>
保存并退出
</button>
</footer>
</section>
</div>
<PuzzleRuntimeDialogButton
tone="primary"
disabled={isBusy}
onClick={() => {
setIsExitRemodelPromptOpen(false);
void onRemodelWork?.(exitPromptProfileId);
}}
className="min-h-[3.25rem] rounded-2xl px-5 text-sm font-black transition hover:brightness-105 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50"
>
作品改造
</PuzzleRuntimeDialogButton>
<PuzzleRuntimeDialogButton
tone="secondary"
onClick={() => {
setIsExitRemodelPromptOpen(false);
onBack();
}}
className="min-h-[3rem] rounded-2xl px-5 text-sm font-bold transition hover:brightness-105 active:translate-y-px"
>
保存并退出
</PuzzleRuntimeDialogButton>
</PuzzleRuntimeDialogFooter>
</PuzzleRuntimeModalShell>
) : null}
{runtimeStatus === 'failed' ? (
<div className="puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm">
<section
role="dialog"
aria-modal="true"
aria-labelledby="puzzle-failed-title"
className="puzzle-runtime-dialog flex w-full max-w-[24rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
>
<header className="puzzle-runtime-dialog__line border-b px-5 py-4">
<h2 id="puzzle-failed-title" className="text-lg font-black">
关卡失败
</h2>
<div className="puzzle-runtime-dialog__soft mt-1 text-xs">
{currentLevel.levelName}
</div>
</header>
<footer className="puzzle-runtime-dialog__line grid grid-cols-2 gap-3 border-t px-5 py-4">
<button
type="button"
disabled={isBusy}
onClick={() => {
void onRestartLevel?.();
}}
className="puzzle-runtime-secondary-button rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
>
重新开始
</button>
<button
type="button"
disabled={isBusy}
onClick={() => openPropDialog('extendTime', '继续1分钟')}
className="puzzle-runtime-primary-button rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
>
继续1分钟
</button>
</footer>
</section>
</div>
<PuzzleRuntimeModalShell
titleId="puzzle-failed-title"
overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm"
dialogClassName="puzzle-runtime-dialog flex w-full max-w-[24rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
>
<header className="puzzle-runtime-dialog__line border-b px-5 py-4">
<h2 id="puzzle-failed-title" className="text-lg font-black">
关卡失败
</h2>
<div className="puzzle-runtime-dialog__soft mt-1 text-xs">
{currentLevel.levelName}
</div>
</header>
<PuzzleRuntimeDialogFooter className="grid grid-cols-2 gap-3 px-5 py-4">
<PuzzleRuntimeDialogButton
tone="secondary"
disabled={isBusy}
onClick={() => {
void onRestartLevel?.();
}}
className="rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
>
重新开始
</PuzzleRuntimeDialogButton>
<PuzzleRuntimeDialogButton
tone="primary"
disabled={isBusy}
onClick={() => openPropDialog('extendTime', '继续1分钟')}
className="rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
>
继续1分钟
</PuzzleRuntimeDialogButton>
</PuzzleRuntimeDialogFooter>
</PuzzleRuntimeModalShell>
) : null}
{clearResultLayer}