fix: 修复开发/测试包模拟进度被易班同步覆盖的问题
进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖; 现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度, 确保测试包的"到达某故居/全流程完成"调试入口生效。 同时纳入近期未提交的页面、组件、资源与环境变量改动。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6d40e19346
commit
49ecabbf3a
155 files changed
+1956
-724
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Image, Title, Cursor, ExhibitFlipOverlay, SceneHint } from '@/components'
|
||||
import { Image, Title, Cursor, ExhibitFlipOverlay, DragFill, SceneHint } from '@/components'
|
||||
import type { DragFillResult } from '@/components'
|
||||
import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { useZhanglepingStore, saveZhanglepingStore } from '@/store/zhanglepingStore'
|
||||
import { SanmaoDialogueItem, SanmaoDialogueSteps } from '@/pages/zhangleping/components'
|
||||
@@ -20,7 +21,7 @@ type ExhibitId = 1 | 2
|
||||
type FragmentId = 3
|
||||
|
||||
const exhibitAssets: Record<ExhibitId, { src: string }> = {
|
||||
1: { src: getStaticImage('zhangleping/reflection/scene3/场景3展品3') },
|
||||
1: { src: getStaticImage('zhangleping/reflection/scene3/场景3展品2') },
|
||||
2: { src: getStaticImage('zhangleping/reflection/scene3/场景3展品1') },
|
||||
}
|
||||
|
||||
@@ -35,32 +36,48 @@ const exhibitDialogues: Record<ExhibitId, Record<string, string>> = {
|
||||
},
|
||||
}
|
||||
|
||||
// 第二阶段:点击常量
|
||||
const CORRECT_CLICK_ITEM = 'click-3'
|
||||
const CLICK_ERROR_HINT =
|
||||
// 第二阶段:拖拽常量
|
||||
const CORRECT_DRAG_OPTION = 'drag-3'
|
||||
const DRAG_ERROR_HINT =
|
||||
'这个物品虽然与三毛有关,但并不是解锁记忆碎片的关键。试试找找与“苦难里还有善良”相关的物品吧!'
|
||||
const PERSISTENT_CLICK_HINT = '点击正确的物品解锁记忆碎片'
|
||||
const CLICK_ERROR_HINT_DURATION_MS = 3200
|
||||
const CLICK_ERROR_HINT_RESET_DELAY_MS = 3300
|
||||
const PERSISTENT_DRAG_HINT = '拖拽正确的物品到这里解锁记忆碎片'
|
||||
const DRAG_ERROR_HINT_DURATION_MS = 3200
|
||||
const DRAG_ERROR_HINT_RESET_DELAY_MS = 3300
|
||||
|
||||
const CLICK_ITEMS = [
|
||||
const DRAG_ITEMS = [
|
||||
{
|
||||
id: 'click-1',
|
||||
id: 'drag-1',
|
||||
className: 'absolute left-[3%] top-[6%]',
|
||||
src: 'zhangleping/reflection/scene3/点击项1',
|
||||
},
|
||||
{
|
||||
id: 'click-2',
|
||||
id: 'drag-2',
|
||||
className: 'absolute left-[36.3%] bottom-[15%]',
|
||||
src: 'zhangleping/reflection/scene3/点击项2',
|
||||
},
|
||||
{
|
||||
id: 'click-3',
|
||||
id: 'drag-3',
|
||||
className: 'absolute left-180 bottom-10',
|
||||
src: 'zhangleping/reflection/scene3/点击项3',
|
||||
},
|
||||
] as const
|
||||
|
||||
type DragOptionValue = (typeof DRAG_ITEMS)[number]['id']
|
||||
|
||||
const DRAG_OPTION_ASSET_MAP: Record<DragOptionValue, { className: string; src: string }> =
|
||||
DRAG_ITEMS.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.id] = { className: item.className, src: item.src }
|
||||
return acc
|
||||
},
|
||||
{} as Record<DragOptionValue, { className: string; src: string }>
|
||||
)
|
||||
|
||||
const DRAG_OPTIONS = DRAG_ITEMS.map((i) => ({ value: i.id, label: i.id }))
|
||||
|
||||
type DragBlankId = 'fragment-drop'
|
||||
const DRAG_BLANKS = [{ id: 'fragment-drop' as DragBlankId, label: '', required: true }]
|
||||
|
||||
const ZhanglepingReflectionScene3 = () => {
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{ moduleName: string }>()
|
||||
@@ -102,33 +119,38 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
visited,
|
||||
})
|
||||
|
||||
// 第二阶段:点击物品
|
||||
const isClickPhase = resourcesCompleted && !fragmentCompleted
|
||||
// 第二阶段:拖拽物品
|
||||
const isDragPhase = resourcesCompleted && !fragmentCompleted
|
||||
const [dragFillValue, setDragFillValue] = useState<DragFillResult[]>([])
|
||||
const [showItemExhibit, setShowItemExhibit] = useState(false)
|
||||
const [showItemDialogue, setShowItemDialogue] = useState(false)
|
||||
const [showFragmentOverlay, setShowFragmentOverlay] = useState(false)
|
||||
const [successHintKey, setSuccessHintKey] = useState(0)
|
||||
|
||||
const hint = useSceneHintController(isClickPhase, PERSISTENT_CLICK_HINT)
|
||||
const hint = useSceneHintController(isDragPhase, PERSISTENT_DRAG_HINT)
|
||||
|
||||
const handleClickItem = useCallback(
|
||||
(itemId: string) => {
|
||||
if (!isClickPhase) return
|
||||
const handleDragValueChange = useCallback(
|
||||
(newValue: DragFillResult[]) => {
|
||||
setDragFillValue(newValue)
|
||||
if (!isDragPhase) return
|
||||
|
||||
if (itemId !== CORRECT_CLICK_ITEM) {
|
||||
const answer = newValue.find((v) => v.blankId === 'fragment-drop')
|
||||
if (!answer) return
|
||||
|
||||
if (answer.optionValue !== CORRECT_DRAG_OPTION) {
|
||||
setDragFillValue([])
|
||||
hint.showTempHint(
|
||||
CLICK_ERROR_HINT,
|
||||
CLICK_ERROR_HINT_DURATION_MS,
|
||||
CLICK_ERROR_HINT_RESET_DELAY_MS
|
||||
DRAG_ERROR_HINT,
|
||||
DRAG_ERROR_HINT_DURATION_MS,
|
||||
DRAG_ERROR_HINT_RESET_DELAY_MS
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 正确:弹出展品与对话(碎片在关闭碎片展品窗口时才解锁)
|
||||
hint.clearAndReset()
|
||||
setShowItemExhibit(true)
|
||||
},
|
||||
[hint, isClickPhase]
|
||||
[hint, isDragPhase]
|
||||
)
|
||||
|
||||
const handleCloseItemExhibit = useCallback(() => {
|
||||
@@ -166,7 +188,7 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat relative"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`${STAR_NAME}/reflection/scene3/${isClickPhase ? '场景-点击' : '场景-进入'}`
|
||||
`${STAR_NAME}/reflection/scene3/${isDragPhase ? '场景-点击' : '场景-进入'}`
|
||||
),
|
||||
}}
|
||||
>
|
||||
@@ -192,11 +214,11 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
苦难善良·记忆碎片已解锁
|
||||
</SceneHint>
|
||||
)}
|
||||
{/* 场景提示:点击阶段常驻/错误提示 */}
|
||||
{/* 场景提示:拖拽阶段常驻/错误提示 */}
|
||||
{!showItemExhibit &&
|
||||
!showFragmentOverlay &&
|
||||
!fragmentCompleted &&
|
||||
isClickPhase &&
|
||||
isDragPhase &&
|
||||
hint.hintMode && (
|
||||
<SceneHint
|
||||
when
|
||||
@@ -209,46 +231,79 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
</SceneHint>
|
||||
)}
|
||||
|
||||
{/* 静态物品(第一阶段纯展示,第二阶段可点击带发光效果) */}
|
||||
{CLICK_ITEMS.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(item.className, isClickPhase && 'cursor-pointer')}
|
||||
onClick={isClickPhase ? () => handleClickItem(item.id) : undefined}
|
||||
style={isClickPhase ? { transition: 'filter 0.2s ease' } : undefined}
|
||||
onMouseEnter={(e) => {
|
||||
if (isClickPhase) {
|
||||
e.currentTarget.style.filter =
|
||||
'drop-shadow(0 0 8px rgba(255, 215, 0, 0.8)) drop-shadow(0 0 16px rgba(255, 215, 0, 0.5))'
|
||||
}
|
||||
{/* 静态物品(第一阶段纯展示,第二阶段可拖拽) */}
|
||||
{!isDragPhase &&
|
||||
DRAG_ITEMS.map((item) => (
|
||||
<Image key={item.src} className={item.className} src={getStaticImage(item.src)} />
|
||||
))}
|
||||
|
||||
{/* 第二阶段:拖拽物品到记忆碎片区域 */}
|
||||
{isDragPhase && (
|
||||
<DragFill
|
||||
options={DRAG_OPTIONS}
|
||||
blanks={DRAG_BLANKS}
|
||||
value={dragFillValue}
|
||||
onChange={handleDragValueChange}
|
||||
showSubmitButton={false}
|
||||
hideLabels
|
||||
layout="horizontal-options"
|
||||
className="absolute inset-0"
|
||||
optionClassName="absolute inset-0"
|
||||
blankClassName="hidden"
|
||||
renderOption={(option, isUsed, isDragging) => {
|
||||
const cfg = DRAG_OPTION_ASSET_MAP[option.value as DragOptionValue]
|
||||
if (!cfg) return null
|
||||
return (
|
||||
<div className={cfg.className} style={{ opacity: isUsed ? 0 : 1 }}>
|
||||
<Image src={getStaticImage(cfg.src)} />
|
||||
{!isUsed && !isDragging && (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute left-1/2 -translate-x-1/2 -bottom-10 z-10',
|
||||
'bg-[#B08D6A] px-4 py-2 rounded-2xl',
|
||||
'whitespace-nowrap text-2xl leading-none text-white'
|
||||
)}
|
||||
>
|
||||
可拖拽
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (isClickPhase) {
|
||||
e.currentTarget.style.filter = 'none'
|
||||
}
|
||||
renderBlank={(blank, _filledValue, isHovered) => (
|
||||
<div
|
||||
data-blank-id={blank.id}
|
||||
className={cn(
|
||||
'absolute left-[42.8%] top-[29%] w-[284px] h-[280px] bg-cover bg-center bg-no-repeat',
|
||||
'flex items-center justify-center pl-2 transition-all duration-150',
|
||||
isHovered && 'scale-105 drop-shadow-[0_0_20px_rgba(255,249,152,0.95)]'
|
||||
)}
|
||||
style={{ backgroundImage: getBackgroundImage('zhangleping/reflection/clue/泡泡') }}
|
||||
>
|
||||
<Image
|
||||
style={{
|
||||
filter: isHovered
|
||||
? 'grayscale(100%) brightness(0) drop-shadow(0 0 12px rgba(255, 249, 152, 0.9))'
|
||||
: 'grayscale(100%) brightness(0)',
|
||||
opacity: isHovered ? 0.9 : 0.7,
|
||||
}}
|
||||
src={getStaticImage('zhangleping/reflection/clue/碎片3')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
renderDragging={(option) => {
|
||||
const src = DRAG_OPTION_ASSET_MAP[option.value as DragOptionValue]?.src
|
||||
if (!src) return null
|
||||
return <img src={getStaticImage(src)} alt="" style={{ width: '8vw', height: 'auto' }} />
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
if (isClickPhase) {
|
||||
e.currentTarget.style.filter =
|
||||
'drop-shadow(0 0 4px rgba(255, 215, 0, 1)) drop-shadow(0 0 10px rgba(255, 215, 0, 0.7))'
|
||||
}
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
if (isClickPhase) {
|
||||
e.currentTarget.style.filter =
|
||||
'drop-shadow(0 0 8px rgba(255, 215, 0, 0.8)) drop-shadow(0 0 16px rgba(255, 215, 0, 0.5))'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image src={getStaticImage(item.src)} />
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 第一阶段:两个光标,按顺序解锁 */}
|
||||
{!resourcesCompleted && (
|
||||
<>
|
||||
<Cursor
|
||||
className="absolute left-195 bottom-15"
|
||||
className="absolute left-198 bottom-50"
|
||||
size={0.6}
|
||||
disabled={!isEnabled(1)}
|
||||
read={visited.has(1)}
|
||||
@@ -264,8 +319,8 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 记忆碎片区域(第二阶段及之后显示) */}
|
||||
{(isClickPhase || fragmentCompleted) && (
|
||||
{/* 记忆碎片区域(解锁后显示) */}
|
||||
{fragmentCompleted && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-[42.8%] top-[29%] w-[284px] h-[280px] bg-cover bg-center bg-no-repeat',
|
||||
@@ -273,17 +328,7 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
)}
|
||||
style={{ backgroundImage: getBackgroundImage('zhangleping/reflection/clue/泡泡') }}
|
||||
>
|
||||
<Image
|
||||
style={
|
||||
fragmentCompleted
|
||||
? undefined
|
||||
: {
|
||||
filter: 'grayscale(100%) brightness(0)',
|
||||
opacity: 0.7,
|
||||
}
|
||||
}
|
||||
src={getStaticImage('zhangleping/reflection/clue/碎片3')}
|
||||
/>
|
||||
<Image src={getStaticImage('zhangleping/reflection/clue/碎片3')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -322,9 +367,9 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
</SanmaoDialogueSteps>
|
||||
)}
|
||||
|
||||
{/* 第二阶段:点击物品2后的展品弹层(场景3展品2) */}
|
||||
{/* 第二阶段:拖拽半块饼后的展品弹层(场景3展品3) */}
|
||||
<ExhibitFlipOverlay
|
||||
src={getStaticImage('zhangleping/reflection/scene3/场景3展品2')}
|
||||
src={getStaticImage('zhangleping/reflection/scene3/场景3展品3')}
|
||||
visible={showItemExhibit}
|
||||
onEnterComplete={() => setShowItemDialogue(true)}
|
||||
onClose={handleCloseItemExhibit}
|
||||
|
||||
Reference in new issue
Block a user