feat:修改英文双引号
This commit is contained in:
commit
4a2be3b4d0
1351 files changed
+56547
No files matched your search
@@ -0,0 +1,432 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
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'
|
||||
import { useCursorController } from '@/hooks'
|
||||
import { useSceneHintController } from './useSceneHintController'
|
||||
|
||||
const STAR_NAME = 'zhangleping'
|
||||
const SCENE = 'scene4'
|
||||
|
||||
// 第一阶段:解锁相关资源(2 个)
|
||||
const RESOURCE_TOTAL = 2
|
||||
// 第二阶段:解锁记忆碎片(1 个)
|
||||
const FRAGMENT_TOTAL = 1
|
||||
|
||||
type ExhibitId = 1 | 2
|
||||
type FragmentId = 3
|
||||
|
||||
const exhibitAssets: Record<ExhibitId, { src: string }> = {
|
||||
1: { src: getStaticImage('zhangleping/reflection/scene4/场景4展品1') },
|
||||
2: { src: getStaticImage('zhangleping/reflection/scene4/场景4展品2') },
|
||||
}
|
||||
|
||||
// 第二阶段:拖拽常量
|
||||
const CORRECT_DRAG_OPTION = 'drag-1'
|
||||
const DRAG_ERROR_HINT =
|
||||
'这个物品虽然与三毛有关,但并不是补全画稿的关键。试试找找与“爱科学的三毛”相关的物品吧!'
|
||||
const PERSISTENT_DRAG_HINT = '拖拽关键物品至虚线框内补全画稿,即可解锁碎片'
|
||||
const DRAG_ERROR_HINT_DURATION_MS = 3200
|
||||
const DRAG_ERROR_HINT_RESET_DELAY_MS = 3300
|
||||
|
||||
const DRAG_ITEMS = [
|
||||
{
|
||||
id: 'drag-1',
|
||||
className: 'absolute left-75 bottom-85',
|
||||
src: 'zhangleping/reflection/scene4/拖拽1',
|
||||
},
|
||||
{
|
||||
id: 'drag-2',
|
||||
className: 'absolute left-160 bottom-65',
|
||||
src: 'zhangleping/reflection/scene4/拖拽2',
|
||||
},
|
||||
{
|
||||
id: 'drag-3',
|
||||
className: 'absolute right-76 bottom-76',
|
||||
src: 'zhangleping/reflection/scene4/拖拽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 = 'drop-1'
|
||||
const DRAG_BLANKS = [{ id: 'drop-1' as DragBlankId, label: '', required: true }]
|
||||
|
||||
const ZhanglepingReflectionScene4 = () => {
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{ moduleName: string }>()
|
||||
|
||||
const { markReflectionCursorVisited, getVisitedReflectionCursors } = useZhanglepingStore()
|
||||
|
||||
// 第一阶段
|
||||
const [activeExhibit, setActiveExhibit] = useState<ExhibitId | null>(null)
|
||||
const [showDialogue, setShowDialogue] = useState(false)
|
||||
const [drawingCompleted, setDrawingCompleted] = useState(false)
|
||||
|
||||
const visitedExhibits = getVisitedReflectionCursors(SCENE)
|
||||
const visited = useMemo(() => new Set(visitedExhibits), [visitedExhibits])
|
||||
|
||||
const markVisitedAndSave = useCallback(
|
||||
(id: ExhibitId | FragmentId) => {
|
||||
markReflectionCursorVisited(SCENE, id)
|
||||
saveZhanglepingStore()
|
||||
},
|
||||
[markReflectionCursorVisited]
|
||||
)
|
||||
|
||||
const resourceProgress = useMemo(
|
||||
() => Array.from(visited).filter((id) => id === 1 || id === 2).length,
|
||||
[visited]
|
||||
)
|
||||
const resourcesCompleted = resourceProgress >= RESOURCE_TOTAL
|
||||
|
||||
const fragmentCompleted = useMemo(() => visited.has(3), [visited])
|
||||
const fragmentProgress = fragmentCompleted ? 1 : 0
|
||||
const completed = fragmentCompleted
|
||||
|
||||
const subtitle = resourcesCompleted
|
||||
? `${fragmentProgress}/${FRAGMENT_TOTAL}`
|
||||
: `${Math.min(resourceProgress, RESOURCE_TOTAL)}/${RESOURCE_TOTAL}`
|
||||
|
||||
const { isEnabled } = useCursorController({
|
||||
unlockRules: [
|
||||
{ id: 2, initialEnabled: true },
|
||||
{ id: 1, dependsOn: [2] },
|
||||
],
|
||||
visited,
|
||||
})
|
||||
|
||||
// 第二阶段:拖拽
|
||||
const [dragFillValue, setDragFillValue] = useState<DragFillResult[]>([])
|
||||
const isDragPhase = resourcesCompleted && !fragmentCompleted
|
||||
const [showItemExhibit, setShowItemExhibit] = useState(false)
|
||||
const [showItemDialogue, setShowItemDialogue] = useState(false)
|
||||
const [showFragmentOverlay, setShowFragmentOverlay] = useState(false)
|
||||
const [successHintKey, setSuccessHintKey] = useState(0)
|
||||
|
||||
const hint = useSceneHintController(isDragPhase, PERSISTENT_DRAG_HINT)
|
||||
|
||||
// 获取放置区域已放置的物品
|
||||
const getBlankFilledItem = useCallback(
|
||||
(blankId: DragBlankId) => {
|
||||
const result = dragFillValue.find((r) => r.blankId === blankId)
|
||||
if (!result) return null
|
||||
return DRAG_OPTION_ASSET_MAP[result.optionValue as DragOptionValue] ?? null
|
||||
},
|
||||
[dragFillValue]
|
||||
)
|
||||
|
||||
const handleDragValueChange = useCallback(
|
||||
(newValue: DragFillResult[]) => {
|
||||
setDragFillValue(newValue)
|
||||
if (!isDragPhase) return
|
||||
|
||||
const answer = newValue.find((v) => v.blankId === 'drop-1')
|
||||
if (!answer) return
|
||||
|
||||
if (answer.optionValue !== CORRECT_DRAG_OPTION) {
|
||||
// 放错:退回并提示
|
||||
setDragFillValue([])
|
||||
hint.showTempHint(
|
||||
DRAG_ERROR_HINT,
|
||||
DRAG_ERROR_HINT_DURATION_MS,
|
||||
DRAG_ERROR_HINT_RESET_DELAY_MS
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 正确:立即完成画稿,再弹出展品与对话
|
||||
setDrawingCompleted(true)
|
||||
setDragFillValue([])
|
||||
hint.clearAndReset()
|
||||
setTimeout(() => setShowItemExhibit(true), 300)
|
||||
},
|
||||
[hint, isDragPhase]
|
||||
)
|
||||
|
||||
const handleCloseItemExhibit = useCallback(() => {
|
||||
setShowItemExhibit(false)
|
||||
setShowItemDialogue(false)
|
||||
setTimeout(() => setShowFragmentOverlay(true), 500)
|
||||
}, [])
|
||||
|
||||
const handleCloseFragmentOverlay = useCallback(() => {
|
||||
setShowFragmentOverlay(false)
|
||||
markVisitedAndSave(3 as FragmentId)
|
||||
setSuccessHintKey((k) => k + 1)
|
||||
}, [markVisitedAndSave])
|
||||
|
||||
const handleLeave = () => navigate(`/${moduleName}/${STAR_NAME}/reflection`)
|
||||
|
||||
const openExhibit = (id: ExhibitId) => {
|
||||
setActiveExhibit(id)
|
||||
setShowDialogue(false)
|
||||
}
|
||||
|
||||
const closeExhibit = () => {
|
||||
if (activeExhibit !== null) {
|
||||
markVisitedAndSave(activeExhibit)
|
||||
}
|
||||
setActiveExhibit(null)
|
||||
setShowDialogue(false)
|
||||
}
|
||||
|
||||
if (!moduleName) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat relative perspective-midrange"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`${STAR_NAME}/reflection/scene4/${isDragPhase ? '场景-拖拽' : '场景-进入'}`
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="新中国"
|
||||
subtitle={subtitle}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
subtitleClassName={completed ? 'text-[#fff998]' : undefined}
|
||||
/>
|
||||
|
||||
{!showItemExhibit && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute right-5 top-5 z-10"
|
||||
src={getStaticImage('components/返回')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 场景提示:碎片已解锁 */}
|
||||
{!showItemExhibit && !showFragmentOverlay && fragmentCompleted && (
|
||||
<SceneHint when triggerKey={successHintKey} persistent offset={{ x: 140 }}>
|
||||
新中国·记忆碎片已解锁
|
||||
</SceneHint>
|
||||
)}
|
||||
{/* 场景提示:拖拽阶段常驻/错误提示 */}
|
||||
{!showItemExhibit &&
|
||||
!showFragmentOverlay &&
|
||||
!fragmentCompleted &&
|
||||
isDragPhase &&
|
||||
hint.hintMode && (
|
||||
<SceneHint
|
||||
when
|
||||
triggerKey={hint.hintTriggerKey}
|
||||
offset={{ x: 140 }}
|
||||
durationMs={hint.hintMode === 'temp' ? hint.hintDurationMs : undefined}
|
||||
persistent={hint.hintMode === 'persistent' ? true : undefined}
|
||||
>
|
||||
{hint.hintText}
|
||||
</SceneHint>
|
||||
)}
|
||||
|
||||
{/* 第一阶段:静态物品(不可拖拽) */}
|
||||
{!isDragPhase &&
|
||||
!fragmentCompleted &&
|
||||
DRAG_ITEMS.map((item) => (
|
||||
<Image key={item.src} className={item.className} src={getStaticImage(item.src)} />
|
||||
))}
|
||||
|
||||
{/* 画稿(未完成/完成) */}
|
||||
<Image
|
||||
className="absolute left-205 bottom-10"
|
||||
src={getStaticImage(
|
||||
`zhangleping/reflection/scene4/${drawingCompleted ? '画稿完成' : '画稿未完成'}`
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 自定义放置区域(拖拽阶段显示) */}
|
||||
{isDragPhase && (
|
||||
<div
|
||||
data-blank-id="drop-1"
|
||||
className={cn(
|
||||
'absolute left-218 bottom-3 z-99',
|
||||
'w-105 h-74',
|
||||
'transform-3d rotate-x-70 -rotate-y-2 rotate-z-3 origin-top',
|
||||
'flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{getBlankFilledItem('drop-1') && (
|
||||
<Image
|
||||
className="h-full object-contain"
|
||||
src={getStaticImage(getBlankFilledItem('drop-1')!.src)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 第二阶段:DragFill 拖拽交互 */}
|
||||
{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-5 z-10',
|
||||
'bg-[#B08D6A] px-4 py-2 rounded-2xl',
|
||||
'whitespace-nowrap text-2xl leading-none text-white'
|
||||
)}
|
||||
>
|
||||
可拖拽
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
renderBlank={(blank) => (
|
||||
<div data-blank-id={blank.id} className="hidden" style={{ display: 'none' }} />
|
||||
)}
|
||||
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: '12vw', height: 'auto' }} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 记忆碎片区域(第二阶段及之后显示) */}
|
||||
{(isDragPhase || fragmentCompleted) && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-240 top-[29%] w-[284px] h-[280px] bg-cover bg-center bg-no-repeat',
|
||||
'flex items-center justify-center pl-2'
|
||||
)}
|
||||
style={{ backgroundImage: getBackgroundImage('zhangleping/reflection/clue/泡泡') }}
|
||||
>
|
||||
<Image
|
||||
style={
|
||||
fragmentCompleted
|
||||
? undefined
|
||||
: {
|
||||
filter: 'grayscale(100%) brightness(0)',
|
||||
opacity: 0.7,
|
||||
}
|
||||
}
|
||||
src={getStaticImage('zhangleping/reflection/clue/碎片4')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 离开场景按钮(碎片展品关闭后出现) */}
|
||||
{fragmentCompleted && !showItemExhibit && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage('components/place/离开场景')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 第一阶段:两个光标,先点光标2再点光标1 */}
|
||||
{!resourcesCompleted && (
|
||||
<>
|
||||
<Cursor
|
||||
className="absolute left-103 bottom-98"
|
||||
size={0.6}
|
||||
disabled={!isEnabled(1)}
|
||||
read={visited.has(1)}
|
||||
onClick={() => openExhibit(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-270 bottom-35"
|
||||
size={0.6}
|
||||
disabled={!isEnabled(2)}
|
||||
read={visited.has(2)}
|
||||
onClick={() => openExhibit(2)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 第一阶段:展品弹层 */}
|
||||
{activeExhibit && (
|
||||
<ExhibitFlipOverlay
|
||||
src={exhibitAssets[activeExhibit].src}
|
||||
visible
|
||||
onEnterComplete={() => {
|
||||
if (activeExhibit === 2) {
|
||||
setShowDialogue(true)
|
||||
}
|
||||
}}
|
||||
onClose={closeExhibit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 第一阶段:光标2的展品对话 */}
|
||||
{activeExhibit === 2 && (
|
||||
<SanmaoDialogueSteps
|
||||
showMask={false}
|
||||
visible={showDialogue}
|
||||
onVisibleChange={setShowDialogue}
|
||||
audioChannelId="zhangleping-exhibit-s4-2"
|
||||
zIndex={10000}
|
||||
>
|
||||
<SanmaoDialogueItem audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_28.mp3')}>
|
||||
1949年以后,我有了新衣服,可以上学了!爷爷说,现在的小朋友不用流浪啦!所以爷爷又创作出了爱科学的我,并在新中国第一份综合性少儿期刊《儿童时代》上连载。
|
||||
</SanmaoDialogueItem>
|
||||
</SanmaoDialogueSteps>
|
||||
)}
|
||||
|
||||
{/* 第二阶段:拖拽成功后的展品弹层(场景4展品3) */}
|
||||
<ExhibitFlipOverlay
|
||||
src={getStaticImage('zhangleping/reflection/scene4/场景4展品3')}
|
||||
visible={showItemExhibit}
|
||||
onEnterComplete={() => setShowItemDialogue(true)}
|
||||
onClose={handleCloseItemExhibit}
|
||||
/>
|
||||
|
||||
{/* 第二阶段:展品对话 */}
|
||||
<SanmaoDialogueSteps
|
||||
showMask={false}
|
||||
visible={showItemDialogue}
|
||||
onVisibleChange={setShowItemDialogue}
|
||||
audioChannelId="zhangleping-exhibit-s4-drag"
|
||||
zIndex={10000}
|
||||
>
|
||||
<SanmaoDialogueItem audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_29.mp3')}>
|
||||
生活在新中国,我要做个爱科学的三毛。
|
||||
</SanmaoDialogueItem>
|
||||
</SanmaoDialogueSteps>
|
||||
|
||||
{/* 第二阶段:碎片展品(碎片4详情) */}
|
||||
<ExhibitFlipOverlay
|
||||
src={getStaticImage('zhangleping/reflection/scene4/碎片4详情')}
|
||||
visible={showFragmentOverlay}
|
||||
onClose={handleCloseFragmentOverlay}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ZhanglepingReflectionScene4
|
||||
Reference in new issue
Block a user