fix: 优化进度调试与答题交互
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d445f81e60
commit
6491b0c918
6 files changed
+378
-151
No files matched your search
@@ -12,6 +12,7 @@ import { Image, PuzzleSystem, flyToAnimation } from '@/components'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { getPuzzlePieceData } from '@/config/puzzleData'
|
||||
import PuzzlePiecePortal from '@/components/puzzle/PuzzlePiecePortal'
|
||||
import { setPreviousPath } from '@/hooks/useResidenceContinue'
|
||||
|
||||
/**
|
||||
* 支持拼图盒功能的故居列表
|
||||
@@ -349,7 +350,8 @@ const ResidenceLayoutContent = () => {
|
||||
setIsVideoFadingOut(false)
|
||||
}
|
||||
|
||||
// 更新前一个路径
|
||||
// 更新前一个路径(供 useResidenceContinue 判断是否从场景回来)
|
||||
setPreviousPath(previousPathRef.current)
|
||||
previousPathRef.current = location.pathname
|
||||
}, [location.pathname, videoSrc, isForwardNavigation])
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import ScaledPortal from '@/components/layout/ScaledPortal'
|
||||
import FlipActionButton from '@/components/button/FlipActionButton'
|
||||
import PuzzlePieceDisplay from './PuzzlePieceDisplay'
|
||||
|
||||
@@ -38,144 +38,144 @@ const PuzzlePiecePortal = forwardRef<HTMLDivElement, PuzzlePiecePortalProps>(
|
||||
return () => clearTimeout(timer)
|
||||
}, [pieceId])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
width: 'min(520px, 86vw)',
|
||||
height: 'min(420px, 72vh)',
|
||||
transform: 'translate(-50%, -50%) translateZ(0)',
|
||||
perspective: '1000px',
|
||||
zIndex: 100001,
|
||||
pointerEvents: 'none',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
{awaitingCollect && !hidden && !confirming && (
|
||||
<motion.div
|
||||
className="absolute left-1/2 top-0 -translate-x-1/2 -z-10"
|
||||
style={{
|
||||
width: 'min(440px, 76vw)',
|
||||
height: '320px',
|
||||
background:
|
||||
'radial-gradient(ellipse 58% 46% at 50% 38%, rgba(255,214,133,0.16) 0%, rgba(255,185,94,0.07) 45%, transparent 76%)',
|
||||
willChange: 'opacity, transform',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.42, ease: [0.22, 1, 0.36, 1] }}
|
||||
/>
|
||||
)}
|
||||
|
||||
return (
|
||||
<ScaledPortal zIndex={100001}>
|
||||
<div
|
||||
className="absolute left-1/2 top-0 -translate-x-1/2"
|
||||
style={{
|
||||
width: 'clamp(240px, 24vw, 300px)',
|
||||
height: 'clamp(190px, 20vw, 240px)',
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
width: 'min(520px, 86vw)',
|
||||
height: 'min(420px, 72vh)',
|
||||
transform: 'translate(-50%, -50%) translateZ(0)',
|
||||
perspective: '1000px',
|
||||
pointerEvents: 'none',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
{awaitingCollect && !hidden && !confirming && (
|
||||
<motion.div
|
||||
className="absolute left-1/2 top-0 -translate-x-1/2 -z-10"
|
||||
style={{
|
||||
width: 'min(440px, 76vw)',
|
||||
height: '320px',
|
||||
background:
|
||||
'radial-gradient(ellipse 58% 46% at 50% 38%, rgba(255,214,133,0.16) 0%, rgba(255,185,94,0.07) 45%, transparent 76%)',
|
||||
willChange: 'opacity, transform',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.42, ease: [0.22, 1, 0.36, 1] }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute left-1/2 top-0 -translate-x-1/2"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transformStyle: 'preserve-3d',
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden',
|
||||
willChange: 'transform, opacity',
|
||||
width: 'clamp(240px, 24vw, 300px)',
|
||||
height: 'clamp(190px, 20vw, 240px)',
|
||||
}}
|
||||
initial={
|
||||
isAnimating
|
||||
? {
|
||||
scale: 0,
|
||||
rotateY: -180,
|
||||
opacity: 0,
|
||||
y: 0,
|
||||
}
|
||||
: false
|
||||
}
|
||||
animate={
|
||||
hidden
|
||||
? { opacity: 0, visibility: 'hidden' as const }
|
||||
: confirming
|
||||
>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transformStyle: 'preserve-3d',
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
initial={
|
||||
isAnimating
|
||||
? {
|
||||
scale: 1.035,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
scale: 0,
|
||||
rotateY: -180,
|
||||
opacity: 0,
|
||||
y: 0,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
: false
|
||||
}
|
||||
animate={
|
||||
hidden
|
||||
? { opacity: 0, visibility: 'hidden' as const }
|
||||
: confirming
|
||||
? {
|
||||
scale: 1.035,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
: {
|
||||
scale: 1,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
}
|
||||
transition={
|
||||
confirming
|
||||
? {
|
||||
duration: 0.18,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
}
|
||||
: {
|
||||
scale: 1,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
}
|
||||
transition={
|
||||
confirming
|
||||
? {
|
||||
duration: 0.18,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
}
|
||||
: {
|
||||
duration: 0.82,
|
||||
ease: [0.2, 0.72, 0.18, 1],
|
||||
scale: {
|
||||
duration: 0.82,
|
||||
ease: [0.2, 0.72, 0.18, 1],
|
||||
},
|
||||
rotateY: {
|
||||
duration: 0.82,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.55,
|
||||
ease: 'easeOut',
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<PuzzlePieceDisplay
|
||||
residence={residence}
|
||||
pieceId={pieceId}
|
||||
showGlow={isAnimating || awaitingCollect}
|
||||
fitContain={true}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{awaitingCollect && !hidden && (
|
||||
<motion.div
|
||||
className="absolute left-0 right-0 top-[clamp(218px,25vw,276px)] flex flex-col items-center gap-3 pointer-events-none"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={confirming ? { opacity: 0, y: 6 } : { opacity: 1, y: 0 }}
|
||||
transition={{ duration: confirming ? 0.16 : 0.36, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ willChange: 'transform, opacity' }}
|
||||
>
|
||||
{puzzlePieceTitle && (
|
||||
<div className="max-w-[480px] rounded-2xl border border-[#f6d58b]/45 bg-[#2a1608]/70 px-6 py-2.5 text-center text-2xl font-bold leading-snug text-[#fff4d0] shadow-[0_6px_20px_rgba(0,0,0,0.22)] [text-shadow:0_2px_0_#5a2a00,0_0_14px_rgba(255,224,150,0.75)]">
|
||||
{puzzlePieceTitle.split('\n').map((line) => (
|
||||
<div key={line}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FlipActionButton
|
||||
className="pointer-events-auto border border-[#ffe6a6]/55 bg-linear-to-b from-[#bd8b50] to-[#84512b] px-4 py-1.5 text-base shadow-[0_3px_10px_rgba(0,0,0,0.2)] hover:brightness-105 active:scale-[0.97]"
|
||||
disabled={confirming}
|
||||
onClick={onCollect}
|
||||
scale: {
|
||||
duration: 0.82,
|
||||
ease: [0.2, 0.72, 0.18, 1],
|
||||
},
|
||||
rotateY: {
|
||||
duration: 0.82,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.55,
|
||||
ease: 'easeOut',
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
收下
|
||||
</FlipActionButton>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
<PuzzlePieceDisplay
|
||||
residence={residence}
|
||||
pieceId={pieceId}
|
||||
showGlow={isAnimating || awaitingCollect}
|
||||
fitContain={true}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{awaitingCollect && !hidden && (
|
||||
<motion.div
|
||||
className="absolute left-0 right-0 top-[clamp(218px,25vw,276px)] flex flex-col items-center gap-3 pointer-events-none"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={confirming ? { opacity: 0, y: 6 } : { opacity: 1, y: 0 }}
|
||||
transition={{ duration: confirming ? 0.16 : 0.36, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ willChange: 'transform, opacity' }}
|
||||
>
|
||||
{puzzlePieceTitle && (
|
||||
<div className="max-w-[480px] rounded-2xl border border-[#f6d58b]/45 bg-[#2a1608]/70 px-6 py-2.5 text-center text-2xl font-bold leading-snug text-[#fff4d0] shadow-[0_6px_20px_rgba(0,0,0,0.22)] [text-shadow:0_2px_0_#5a2a00,0_0_14px_rgba(255,224,150,0.75)]">
|
||||
{puzzlePieceTitle.split('\n').map((line) => (
|
||||
<div key={line}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FlipActionButton
|
||||
className="pointer-events-auto border border-[#ffe6a6]/55 bg-linear-to-b from-[#bd8b50] to-[#84512b] px-4 py-1.5 text-base shadow-[0_3px_10px_rgba(0,0,0,0.2)] hover:brightness-105 active:scale-[0.97]"
|
||||
disabled={confirming}
|
||||
onClick={onCollect}
|
||||
>
|
||||
收下
|
||||
</FlipActionButton>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</ScaledPortal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -296,7 +296,11 @@ const Video = forwardRef<VideoRef, VideoProps>(
|
||||
for (const anchor of anchors) {
|
||||
if (!isAnchorTriggerable(anchor)) continue
|
||||
const timeDiff = time - anchor.time
|
||||
if (timeDiff >= 0 && timeDiff < 0.5) {
|
||||
if (timeDiff >= 0) {
|
||||
// 时间跳变过大(如退后台返回)时回拨到锚点位置,保证题在准确时刻弹出
|
||||
if (timeDiff > 1.5) {
|
||||
video.currentTime = anchor.time
|
||||
}
|
||||
triggerAnchor(anchor, video)
|
||||
break
|
||||
}
|
||||
@@ -310,7 +314,7 @@ const Video = forwardRef<VideoRef, VideoProps>(
|
||||
for (const anchor of anchors) {
|
||||
if (!isAnchorTriggerable(anchor)) continue
|
||||
const timeDiff = targetTime - anchor.time
|
||||
if (timeDiff >= 0 && timeDiff < 0.5) {
|
||||
if (timeDiff >= 0) {
|
||||
triggerAnchor(anchor, videoRef.current)
|
||||
break
|
||||
}
|
||||
@@ -577,8 +581,7 @@ const Video = forwardRef<VideoRef, VideoProps>(
|
||||
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0
|
||||
|
||||
// 视频首次观看限制:env 默认值可被调试面板运行时覆盖
|
||||
const envDisableGating =
|
||||
import.meta.env.VITE_DISABLE_FIRST_WATCH_VIDEO_GATING === 'true'
|
||||
const envDisableGating = import.meta.env.VITE_DISABLE_FIRST_WATCH_VIDEO_GATING === 'true'
|
||||
const sessionOverride = window.sessionStorage.getItem('debug:video-gating')
|
||||
const disableFirstWatchVideoGating =
|
||||
sessionOverride !== null ? sessionOverride === 'disabled' : envDisableGating
|
||||
|
||||
Reference in new issue
Block a user