1
This commit is contained in:
@@ -41,6 +41,7 @@ type PuzzleRuntimeShellProps = {
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
hideBackButton?: boolean;
|
||||
hideExitControls?: boolean;
|
||||
embedded?: boolean;
|
||||
onBack: () => void;
|
||||
onRemodelWork?: (profileId: string) => void | Promise<void>;
|
||||
@@ -157,10 +158,6 @@ function formatTimerMs(value: number | null | undefined) {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function resolveAuthorAvatarLabel(authorDisplayName: string) {
|
||||
return authorDisplayName.trim().slice(0, 1) || '玩';
|
||||
}
|
||||
|
||||
function resolveActiveFreezeElapsedMs(
|
||||
level: PuzzleRuntimeLevelSnapshot,
|
||||
nowMs: number,
|
||||
@@ -309,6 +306,7 @@ export function PuzzleRuntimeShell({
|
||||
isBusy = false,
|
||||
error = null,
|
||||
hideBackButton = false,
|
||||
hideExitControls = false,
|
||||
embedded = false,
|
||||
onBack,
|
||||
onRemodelWork,
|
||||
@@ -790,9 +788,9 @@ export function PuzzleRuntimeShell({
|
||||
if (!run || !currentLevel || !board) {
|
||||
return (
|
||||
<div
|
||||
className={`${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex items-center justify-center bg-slate-950 text-white`}
|
||||
className={`puzzle-runtime-shell ${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex items-center justify-center`}
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded-full bg-white/10 px-5 py-3 text-sm">
|
||||
<div className="puzzle-runtime-pill flex items-center gap-2 rounded-full px-5 py-3 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
正在进入拼图关卡
|
||||
</div>
|
||||
@@ -963,9 +961,6 @@ export function PuzzleRuntimeShell({
|
||||
const canShowNextAction =
|
||||
canAdvanceDefaultNextLevel || hasSimilarWorkChoices;
|
||||
const levelLabel = `第 ${currentLevel.levelIndex} 关`;
|
||||
const authorAvatarLabel = resolveAuthorAvatarLabel(
|
||||
currentLevel.authorDisplayName,
|
||||
);
|
||||
const exitPromptProfileId = currentLevel.profileId.trim();
|
||||
const leaderboardEntries =
|
||||
(currentLevel.leaderboardEntries ?? []).length > 0
|
||||
@@ -979,6 +974,10 @@ export function PuzzleRuntimeShell({
|
||||
isBusy || runtimeStatus !== 'playing' || Boolean(propDialog);
|
||||
|
||||
const handleBackRequest = () => {
|
||||
if (hideExitControls) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
onRemodelWork &&
|
||||
exitPromptProfileId &&
|
||||
@@ -1069,7 +1068,10 @@ export function PuzzleRuntimeShell({
|
||||
if (propKind === 'freezeTime') {
|
||||
// 中文注释:正式 run 可能在冻结确认期间已被服务端结算为失败态;
|
||||
// 这种边界同步只关闭确认窗,不再播放冻结成功反馈。
|
||||
const resultLevel = (useResult ?? null)?.currentLevel ?? currentLevelRef.current;
|
||||
const resultLevel =
|
||||
useResult && typeof useResult === 'object'
|
||||
? useResult.currentLevel
|
||||
: currentLevelRef.current;
|
||||
if (resultLevel?.status === 'playing') {
|
||||
setIsFreezeEffectVisible(true);
|
||||
window.setTimeout(() => {
|
||||
@@ -1084,9 +1086,9 @@ export function PuzzleRuntimeShell({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex justify-center bg-slate-950 text-white`}
|
||||
className={`puzzle-runtime-shell ${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex justify-center`}
|
||||
>
|
||||
<div className="relative h-full w-full overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(251,191,36,0.18),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.16),transparent_26%),linear-gradient(180deg,#2d160e,#020617)]">
|
||||
<div className="puzzle-runtime-stage relative h-full w-full overflow-hidden">
|
||||
{currentLevel.coverImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentLevel.coverImageSrc}
|
||||
@@ -1095,49 +1097,40 @@ export function PuzzleRuntimeShell({
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-[0.16] blur-2xl"
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:34px_34px] opacity-20" />
|
||||
<div className="puzzle-runtime-stage__grid" />
|
||||
|
||||
<div className="absolute left-0 top-0 z-20 w-full px-4 py-4">
|
||||
<div className="grid grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] items-start gap-2 sm:gap-3">
|
||||
<div className="absolute left-0 top-0 z-20 w-full px-3 py-3 sm:px-4">
|
||||
<div className="grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-start gap-2 sm:grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBackRequest}
|
||||
aria-label="返回上一页"
|
||||
disabled={hideBackButton}
|
||||
className={`h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur ${
|
||||
className={`puzzle-runtime-icon-button h-10 w-10 items-center justify-center rounded-full sm:h-11 sm:w-11 ${
|
||||
hideBackButton ? 'invisible pointer-events-none' : 'inline-flex'
|
||||
}`}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex min-w-0 flex-col items-center gap-2 rounded-[1.35rem] bg-black/30 px-3 py-3 text-center backdrop-blur sm:px-5">
|
||||
<div className="line-clamp-1 max-w-full text-sm font-black text-white sm:text-base">
|
||||
{currentLevel.levelName}
|
||||
</div>
|
||||
<div
|
||||
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 font-mono text-2xl font-black leading-none shadow-[0_10px_28px_rgba(0,0,0,0.24)] sm:text-3xl ${
|
||||
displayRemainingMs <= 20_000 && runtimeStatus === 'playing'
|
||||
? 'bg-red-500/24 text-red-100'
|
||||
: 'bg-white/12 text-white'
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-5 w-5 sm:h-6 sm:w-6" />
|
||||
{formatTimerMs(displayRemainingMs)}
|
||||
</div>
|
||||
<div className="flex min-w-0 max-w-full items-center justify-center gap-2 text-white/82">
|
||||
<span
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/16 bg-amber-200 text-xs font-black text-slate-950 shadow-[0_8px_20px_rgba(0,0,0,0.2)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{authorAvatarLabel}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-xs font-semibold sm:text-sm">
|
||||
{currentLevel.authorDisplayName}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-bold tracking-[0.12em] text-amber-100/90 sm:text-[11px]">
|
||||
<div className="puzzle-runtime-header-card mx-auto flex max-w-[min(15rem,calc(100vw_-_6.5rem))] min-w-0 flex-col items-center gap-1.5 rounded-[1.1rem] px-3 py-2 text-center sm:max-w-[18rem] sm:px-4">
|
||||
<div className="flex max-w-full items-center justify-center gap-1.5">
|
||||
<span className="puzzle-runtime-level-badge shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold sm:text-[11px]">
|
||||
{levelLabel}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-sm font-black sm:text-base">
|
||||
{currentLevel.levelName}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 font-mono text-lg font-black leading-none shadow-[0_10px_28px_rgba(0,0,0,0.2)] sm:text-xl ${
|
||||
displayRemainingMs <= 20_000 && runtimeStatus === 'playing'
|
||||
? 'puzzle-runtime-timer--urgent'
|
||||
: 'puzzle-runtime-timer'
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
{formatTimerMs(displayRemainingMs)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1146,21 +1139,21 @@ export function PuzzleRuntimeShell({
|
||||
onClick={() => setIsSettingsPanelOpen(true)}
|
||||
aria-label="打开拼图设置"
|
||||
title="打开拼图设置"
|
||||
className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-200/60"
|
||||
className="puzzle-runtime-icon-button inline-flex h-10 w-10 items-center justify-center rounded-full transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-button-primary-border)] sm:h-11 sm:w-11"
|
||||
>
|
||||
<PixelIcon
|
||||
src={CHROME_ICONS.settings}
|
||||
className="h-[1.4rem] w-[1.4rem] drop-shadow-[0_4px_10px_rgba(0,0,0,0.45)]"
|
||||
className="h-5 w-5 drop-shadow-[0_4px_10px_rgba(0,0,0,0.45)] sm:h-[1.4rem] sm:w-[1.4rem]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center px-1 py-3 pt-28 pb-32 sm:p-4">
|
||||
<div className="absolute inset-0 flex items-center justify-center px-1 py-3 pt-24 pb-32 sm:px-4 sm:py-4 sm:pt-24 sm:pb-28">
|
||||
<div
|
||||
ref={boardRef}
|
||||
data-testid="puzzle-board"
|
||||
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]"
|
||||
className="puzzle-runtime-board relative grid aspect-square w-full max-w-[min(99vw,calc(100vh_-_14rem))] touch-none select-none overflow-hidden rounded-[1.2rem] border backdrop-blur-sm sm:max-w-[min(92vw,calc(100vh_-_13rem))] sm:rounded-[1.45rem]"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${board.cols}, minmax(0, 1fr))`,
|
||||
gridTemplateRows: `repeat(${board.rows}, minmax(0, 1fr))`,
|
||||
@@ -1216,14 +1209,14 @@ export function PuzzleRuntimeShell({
|
||||
pieceElementRefMap.current.delete(piece.pieceId);
|
||||
}}
|
||||
data-piece-id={piece?.pieceId ?? undefined}
|
||||
className={`relative flex h-full items-center justify-center overflow-hidden rounded-[0.85rem] border-2 border-white/22 text-sm font-black transition ${
|
||||
className={`puzzle-runtime-piece relative flex h-full items-center justify-center overflow-hidden rounded-[0.85rem] border-2 text-sm font-black transition ${
|
||||
occupied
|
||||
? isSelected
|
||||
? 'border-amber-200 bg-amber-400/84 text-slate-950 shadow-[0_12px_30px_rgba(251,191,36,0.22)]'
|
||||
? 'puzzle-runtime-piece--selected'
|
||||
: isMerged
|
||||
? 'border-transparent bg-transparent text-white'
|
||||
: 'bg-white/12 text-white'
|
||||
: 'border-white/8 bg-black/18 text-white/20'
|
||||
? 'puzzle-runtime-piece--merged'
|
||||
: 'puzzle-runtime-piece--filled'
|
||||
: 'puzzle-runtime-piece--empty'
|
||||
} ${
|
||||
isMerged
|
||||
? 'transition-colors'
|
||||
@@ -1282,7 +1275,7 @@ export function PuzzleRuntimeShell({
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[linear-gradient(145deg,rgba(251,191,36,0.4),rgba(76,29,19,0.72))]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/10" />
|
||||
<div className="puzzle-runtime-piece-overlay absolute inset-0" />
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
@@ -1467,12 +1460,12 @@ export function PuzzleRuntimeShell({
|
||||
|
||||
<div className="absolute bottom-0 left-0 z-20 flex w-full flex-col items-center gap-2 px-3 py-3 sm:px-4 sm:py-4">
|
||||
{error ? (
|
||||
<div className="rounded-full bg-red-500/20 px-3 py-1 text-xs text-red-100">
|
||||
<div className="puzzle-runtime-error-chip rounded-full px-3 py-1 text-xs">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedPieceId && runtimeStatus === 'playing' ? (
|
||||
<div className="rounded-full bg-black/28 px-3 py-1 text-xs text-white/72 backdrop-blur">
|
||||
<div className="puzzle-runtime-status-chip rounded-full px-3 py-1 text-xs">
|
||||
已选择
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1491,21 +1484,21 @@ export function PuzzleRuntimeShell({
|
||||
levelId: run.nextLevelId ?? null,
|
||||
});
|
||||
}}
|
||||
className="inline-flex min-h-11 items-center gap-2 rounded-full bg-amber-200 px-5 py-2.5 text-sm font-bold text-slate-950 shadow-[0_14px_36px_rgba(251,191,36,0.26)] transition hover:bg-amber-100 disabled:opacity-45"
|
||||
className="puzzle-runtime-primary-button inline-flex min-h-11 items-center gap-2 rounded-full px-5 py-2.5 text-sm font-bold transition hover:brightness-105 disabled:opacity-45"
|
||||
>
|
||||
{hasSimilarWorkChoices ? '换个作品' : '下一关'}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center justify-center gap-2 rounded-full bg-black/36 p-2 backdrop-blur sm:gap-3">
|
||||
<div className="puzzle-runtime-toolbar flex items-center justify-center gap-2 rounded-full p-2 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isInteractionLocked}
|
||||
onClick={() => openPropDialog('hint', '使用提示')}
|
||||
className="inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black text-white/88 transition hover:bg-white/10 disabled:opacity-45"
|
||||
className="puzzle-runtime-tool-button inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black transition disabled:opacity-45"
|
||||
>
|
||||
<Lightbulb className="h-6 w-6 text-amber-100" />
|
||||
<Lightbulb className="puzzle-runtime-tool-button__warm h-6 w-6" />
|
||||
提示
|
||||
</button>
|
||||
<button
|
||||
@@ -1519,10 +1512,10 @@ export function PuzzleRuntimeShell({
|
||||
}
|
||||
openPropDialog('reference', '查看原图');
|
||||
}}
|
||||
className={`inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black transition hover:bg-white/10 disabled:opacity-45 ${
|
||||
className={`inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black transition disabled:opacity-45 ${
|
||||
isOriginalOverlayVisible
|
||||
? 'bg-sky-200 text-slate-950'
|
||||
: 'text-white/86'
|
||||
? 'puzzle-runtime-tool-button--active'
|
||||
: 'puzzle-runtime-tool-button'
|
||||
}`}
|
||||
>
|
||||
<Eye className="h-6 w-6" />
|
||||
@@ -1532,9 +1525,9 @@ export function PuzzleRuntimeShell({
|
||||
type="button"
|
||||
disabled={isInteractionLocked}
|
||||
onClick={() => openPropDialog('freezeTime', '冻结时间')}
|
||||
className="inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black text-white/88 transition hover:bg-white/10 disabled:opacity-45"
|
||||
className="puzzle-runtime-tool-button inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black transition disabled:opacity-45"
|
||||
>
|
||||
<Snowflake className="h-6 w-6 text-cyan-100" />
|
||||
<Snowflake className="puzzle-runtime-tool-button__cool h-6 w-6" />
|
||||
冻结
|
||||
</button>
|
||||
</div>
|
||||
@@ -1565,7 +1558,7 @@ export function PuzzleRuntimeShell({
|
||||
|
||||
{propDialog ? (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
|
||||
className="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm"
|
||||
onClick={() => {
|
||||
if (!isPropConfirming) {
|
||||
setPropDialog(null);
|
||||
@@ -1576,35 +1569,35 @@ export function PuzzleRuntimeShell({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="puzzle-prop-confirm-title"
|
||||
className="pixel-nine-slice pixel-modal-shell w-full max-w-[22rem] overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
||||
className="puzzle-runtime-dialog pixel-nine-slice pixel-modal-shell w-full max-w-[22rem] overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
|
||||
style={getNineSliceStyle(UI_CHROME.modalPanel)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="flex items-center gap-3 border-b border-white/10 px-5 py-4">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-200 text-slate-950">
|
||||
<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 text-white"
|
||||
className="text-sm font-black"
|
||||
>
|
||||
{propDialog.title}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="px-5 py-4 text-sm text-white/72">
|
||||
<div className="puzzle-runtime-dialog__body px-5 py-4 text-sm">
|
||||
消耗 1 光点
|
||||
{propConfirmError ? (
|
||||
<div className="mt-3 rounded-[0.9rem] border border-red-300/20 bg-red-500/12 px-3 py-2 text-xs leading-5 text-red-100">
|
||||
<div className="puzzle-runtime-error-chip mt-3 rounded-[0.9rem] border px-3 py-2 text-xs leading-5">
|
||||
{propConfirmError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<footer className="flex items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
|
||||
<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="rounded-full border border-white/12 bg-black/20 px-4 py-2 text-xs font-bold text-zinc-200 transition hover:text-white"
|
||||
className="puzzle-runtime-secondary-button rounded-full px-4 py-2 text-xs font-bold transition hover:brightness-105"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -1614,7 +1607,7 @@ export function PuzzleRuntimeShell({
|
||||
onClick={() => {
|
||||
void handleConfirmProp();
|
||||
}}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-5 py-2 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:opacity-60"
|
||||
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" />
|
||||
@@ -1628,51 +1621,53 @@ export function PuzzleRuntimeShell({
|
||||
|
||||
{isSettingsPanelOpen ? (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex items-center justify-center bg-black/72 p-3 backdrop-blur-sm sm:p-4"
|
||||
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)}
|
||||
>
|
||||
<section
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="puzzle-settings-title"
|
||||
className="pixel-nine-slice pixel-modal-shell flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
||||
className="puzzle-runtime-dialog pixel-nine-slice pixel-modal-shell flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
|
||||
style={getNineSliceStyle(UI_CHROME.modalPanel)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
|
||||
<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 text-white"
|
||||
className="text-sm font-semibold"
|
||||
>
|
||||
拼图设置
|
||||
</h2>
|
||||
<div className="mt-1 text-[11px] text-zinc-500">
|
||||
调整音乐音量,查看本局进度,或返回上一页。
|
||||
<div className="puzzle-runtime-dialog__soft mt-1 text-[11px]">
|
||||
{hideExitControls
|
||||
? '调整音乐音量,查看本局进度。'
|
||||
: '调整音乐音量,查看本局进度,或返回上一页。'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭拼图设置"
|
||||
onClick={() => setIsSettingsPanelOpen(false)}
|
||||
className="absolute right-4 top-3 p-1 text-zinc-400 transition-colors hover:text-white sm:right-5 sm:top-4"
|
||||
className="puzzle-runtime-dialog__soft absolute right-4 top-3 p-1 transition-colors hover:brightness-75 sm:right-5 sm:top-4"
|
||||
>
|
||||
<PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
|
||||
<div className="rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.14),transparent_65%),rgba(0,0,0,0.24)] p-4">
|
||||
<div className="puzzle-runtime-settings-card rounded-2xl p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[10px] tracking-[0.24em] text-sky-200/80">
|
||||
<div className="puzzle-runtime-tool-button__cool text-[10px] tracking-[0.24em]">
|
||||
音频
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-semibold text-white">
|
||||
<div className="mt-2 text-sm font-semibold">
|
||||
音乐音量
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-black/28 px-2 py-1 text-xs text-white/80">
|
||||
<div className="puzzle-runtime-pill rounded-full px-2 py-1 text-xs">
|
||||
{Math.round(musicVolume * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -1690,37 +1685,37 @@ export function PuzzleRuntimeShell({
|
||||
Number(event.currentTarget.value) / 100,
|
||||
)
|
||||
}
|
||||
className="h-2 w-full cursor-pointer accent-sky-400"
|
||||
className="h-2 w-full cursor-pointer accent-[var(--puzzle-runtime-accent-text)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-black/25 px-4 py-3">
|
||||
<div className="text-[10px] uppercase tracking-[0.18em] text-zinc-500">
|
||||
<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="mt-3 space-y-2 text-sm text-white/82">
|
||||
<div className="puzzle-runtime-dialog__body mt-3 space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-white/56">关卡</span>
|
||||
<span className="font-semibold text-white">
|
||||
<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="text-white/56">已完成关卡</span>
|
||||
<span className="font-semibold text-white">
|
||||
<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="text-white/56">当前状态</span>
|
||||
<span className="font-semibold text-white">
|
||||
<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="text-white/56">当前用时</span>
|
||||
<span className="font-mono font-semibold text-white">
|
||||
<span className="puzzle-runtime-dialog__soft">当前用时</span>
|
||||
<span className="font-mono font-semibold">
|
||||
{formatElapsedMs(currentLevel.elapsedMs)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1728,50 +1723,52 @@ export function PuzzleRuntimeShell({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="flex items-center justify-end gap-3 border-t border-white/10 px-4 py-3 sm:px-5">
|
||||
<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="rounded-full border border-white/12 bg-black/20 px-3 py-1.5 text-[11px] text-zinc-200 transition hover:text-white"
|
||||
className="puzzle-runtime-secondary-button rounded-full px-3 py-1.5 text-[11px] transition hover:brightness-105"
|
||||
>
|
||||
继续拼图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsSettingsPanelOpen(false);
|
||||
onBack();
|
||||
}}
|
||||
className={`rounded-full bg-amber-200 px-4 py-2 text-sm font-bold text-slate-950 transition hover:bg-amber-100 ${
|
||||
hideBackButton ? '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 ${
|
||||
hideBackButton ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
返回上一页
|
||||
</button>
|
||||
) : null}
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isExitRemodelPromptOpen ? (
|
||||
{isExitRemodelPromptOpen && !hideExitControls ? (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex items-center justify-center bg-slate-950/76 px-4 py-6 backdrop-blur-md"
|
||||
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="relative flex w-full max-w-[21rem] flex-col overflow-hidden rounded-[1.35rem] border border-amber-200/24 bg-[linear-gradient(180deg,rgba(30,41,59,0.98),rgba(2,6,23,0.98))] shadow-[0_28px_90px_rgba(0,0,0,0.58)]"
|
||||
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()}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-amber-200/70 to-transparent" />
|
||||
<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="mb-4 grid h-14 w-14 place-items-center rounded-2xl border border-amber-200/28 bg-amber-200/12 shadow-[0_16px_42px_rgba(251,191,36,0.18)]">
|
||||
<Sparkles className="h-7 w-7 text-amber-200" />
|
||||
<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] text-white"
|
||||
className="text-[1.75rem] font-black leading-[1.08]"
|
||||
>
|
||||
体验不佳?
|
||||
<br />
|
||||
@@ -1786,7 +1783,7 @@ export function PuzzleRuntimeShell({
|
||||
setIsExitRemodelPromptOpen(false);
|
||||
void onRemodelWork?.(exitPromptProfileId);
|
||||
}}
|
||||
className="min-h-[3.25rem] rounded-2xl bg-amber-200 px-5 text-sm font-black text-slate-950 shadow-[0_14px_34px_rgba(251,191,36,0.24)] transition hover:bg-amber-100 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50"
|
||||
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>
|
||||
@@ -1796,7 +1793,7 @@ export function PuzzleRuntimeShell({
|
||||
setIsExitRemodelPromptOpen(false);
|
||||
onBack();
|
||||
}}
|
||||
className="min-h-[3rem] rounded-2xl border border-white/14 bg-white/8 px-5 text-sm font-bold text-white/92 transition hover:bg-white/12 active:translate-y-px"
|
||||
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>
|
||||
@@ -1806,32 +1803,32 @@ export function PuzzleRuntimeShell({
|
||||
) : 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">
|
||||
<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="flex w-full max-w-[24rem] 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)]"
|
||||
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="border-b border-white/10 px-5 py-4">
|
||||
<header className="puzzle-runtime-dialog__line border-b px-5 py-4">
|
||||
<h2
|
||||
id="puzzle-failed-title"
|
||||
className="text-lg font-black text-white"
|
||||
className="text-lg font-black"
|
||||
>
|
||||
关卡失败
|
||||
</h2>
|
||||
<div className="mt-1 text-xs text-white/62">
|
||||
<div className="puzzle-runtime-dialog__soft mt-1 text-xs">
|
||||
{currentLevel.levelName}
|
||||
</div>
|
||||
</header>
|
||||
<footer className="grid grid-cols-2 gap-3 border-t border-white/10 px-5 py-4">
|
||||
<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="rounded-full border border-white/14 bg-black/24 px-4 py-2.5 text-sm font-black text-white transition hover:bg-white/10 disabled:opacity-50"
|
||||
className="puzzle-runtime-secondary-button rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
|
||||
>
|
||||
重新开始
|
||||
</button>
|
||||
@@ -1839,7 +1836,7 @@ export function PuzzleRuntimeShell({
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => openPropDialog('extendTime', '继续1分钟')}
|
||||
className="rounded-full bg-amber-200 px-4 py-2.5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:opacity-50"
|
||||
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>
|
||||
@@ -1849,32 +1846,32 @@ export function PuzzleRuntimeShell({
|
||||
) : null}
|
||||
|
||||
{isClearResultOpen ? (
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center bg-slate-950/68 px-4 py-6 backdrop-blur-sm">
|
||||
<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-clear-result-title"
|
||||
className="flex max-h-[min(92vh,42rem)] w-full max-w-[28rem] 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)]"
|
||||
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)]"
|
||||
>
|
||||
<header className="flex items-start justify-between gap-3 border-b border-white/10 px-5 py-4">
|
||||
<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="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-200 text-slate-950">
|
||||
<div className="puzzle-runtime-primary-button mb-2 inline-flex h-9 w-9 items-center justify-center rounded-full">
|
||||
<Trophy className="h-4 w-4" />
|
||||
</div>
|
||||
<h2
|
||||
id="puzzle-clear-result-title"
|
||||
className="truncate text-lg font-black text-white"
|
||||
className="truncate text-lg font-black"
|
||||
>
|
||||
通关完成
|
||||
</h2>
|
||||
<div className="mt-1 line-clamp-1 text-xs text-white/62">
|
||||
<div className="puzzle-runtime-dialog__soft mt-1 line-clamp-1 text-xs">
|
||||
{currentLevel.levelName}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭通关弹窗"
|
||||
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/8 text-white/72 transition hover:bg-white/14 hover:text-white"
|
||||
className="puzzle-runtime-secondary-button inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition hover:brightness-105"
|
||||
onClick={() => {
|
||||
setDismissedClearKey(clearResultKey);
|
||||
}}
|
||||
@@ -1884,26 +1881,26 @@ export function PuzzleRuntimeShell({
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="flex items-center justify-between gap-4 rounded-[1rem] border border-amber-200/24 bg-amber-200/10 px-4 py-3">
|
||||
<div className="puzzle-runtime-stat-card flex items-center justify-between gap-4 rounded-[1rem] px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/24 text-amber-100">
|
||||
<span className="puzzle-runtime-pill inline-flex h-9 w-9 items-center justify-center rounded-full">
|
||||
<Clock className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white/72">
|
||||
<span className="puzzle-runtime-dialog__soft text-sm font-semibold">
|
||||
通关时间
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-xl font-black text-amber-100">
|
||||
<span className="puzzle-runtime-tool-button__warm font-mono text-xl font-black">
|
||||
{formatElapsedMs(currentLevel.elapsedMs)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mb-2 text-sm font-bold text-white">
|
||||
<div className="mb-2 text-sm font-bold">
|
||||
排行榜
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[1rem] border border-white/10">
|
||||
<div className="grid grid-cols-[3.5rem_minmax(0,1fr)_6rem] bg-white/6 px-3 py-2 text-[11px] font-bold text-white/48">
|
||||
<div className="puzzle-runtime-dialog__line overflow-hidden rounded-[1rem] border">
|
||||
<div className="puzzle-runtime-leaderboard-head grid grid-cols-[3.5rem_minmax(0,1fr)_6rem] px-3 py-2 text-[11px] font-bold">
|
||||
<span>名次</span>
|
||||
<span>昵称</span>
|
||||
<span className="text-right">通关时间</span>
|
||||
@@ -1915,8 +1912,8 @@ export function PuzzleRuntimeShell({
|
||||
key={`${entry.rank}:${entry.nickname}:${entry.elapsedMs}`}
|
||||
className={`grid grid-cols-[3.5rem_minmax(0,1fr)_6rem] items-center px-3 py-2.5 text-sm ${
|
||||
entry.isCurrentPlayer
|
||||
? 'bg-amber-200/14 text-amber-50'
|
||||
: 'border-t border-white/8 text-white/78'
|
||||
? 'puzzle-runtime-leaderboard-row--active'
|
||||
: 'puzzle-runtime-leaderboard-row border-t'
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono font-black">
|
||||
@@ -1931,7 +1928,7 @@ export function PuzzleRuntimeShell({
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-24 items-center justify-center px-4 py-5 text-sm text-white/56">
|
||||
<div className="puzzle-runtime-dialog__soft flex min-h-24 items-center justify-center px-4 py-5 text-sm">
|
||||
{isBusy
|
||||
? '正在同步真实排行榜…'
|
||||
: '暂无真实排行榜成绩'}
|
||||
@@ -1960,7 +1957,7 @@ export function PuzzleRuntimeShell({
|
||||
</div>
|
||||
|
||||
{canAdvanceDefaultNextLevel ? (
|
||||
<footer className="flex items-center justify-end border-t border-white/10 px-5 py-4">
|
||||
<footer className="puzzle-runtime-dialog__line flex items-center justify-end border-t px-5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
@@ -1970,7 +1967,7 @@ export function PuzzleRuntimeShell({
|
||||
levelId: run.nextLevelId ?? null,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-5 py-2.5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="puzzle-runtime-primary-button inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-black transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{isBusy ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -2003,9 +2000,9 @@ function PuzzleNextWorkCard({
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className="group grid min-h-[5.75rem] grid-cols-[4.5rem_minmax(0,1fr)] overflow-hidden rounded-[1rem] border border-white/10 bg-white/6 text-left transition hover:border-amber-200/40 hover:bg-amber-200/10 disabled:cursor-not-allowed disabled:opacity-45 sm:grid-cols-1"
|
||||
className="puzzle-runtime-next-card group grid min-h-[5.75rem] grid-cols-[4.5rem_minmax(0,1fr)] overflow-hidden rounded-[1rem] text-left transition disabled:cursor-not-allowed disabled:opacity-45 sm:grid-cols-1"
|
||||
>
|
||||
<div className="relative min-h-full bg-white/8 sm:aspect-[1.35]">
|
||||
<div className="puzzle-runtime-next-card__media relative min-h-full sm:aspect-[1.35]">
|
||||
{item.coverImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={item.coverImageSrc}
|
||||
@@ -2015,20 +2012,20 @@ function PuzzleNextWorkCard({
|
||||
) : (
|
||||
<div className="h-full w-full bg-[linear-gradient(145deg,rgba(20,184,166,0.34),rgba(15,23,42,0.88))]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/10 transition group-hover:bg-black/0" />
|
||||
<div className="puzzle-runtime-piece-overlay absolute inset-0 transition group-hover:opacity-0" />
|
||||
</div>
|
||||
<div className="min-w-0 px-3 py-2.5">
|
||||
<div className="truncate text-sm font-black text-white">
|
||||
<div className="truncate text-sm font-black">
|
||||
{item.levelName}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs font-semibold text-white/58">
|
||||
<div className="puzzle-runtime-dialog__soft mt-1 truncate text-xs font-semibold">
|
||||
{item.authorDisplayName}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{item.themeTags.slice(0, 2).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="max-w-full truncate rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-bold text-white/64"
|
||||
className="puzzle-runtime-next-card__tag max-w-full truncate rounded-full px-2 py-0.5 text-[10px] font-bold"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user