feat:修改英文双引号

This commit is contained in:
kevinzhu committed 2026-04-30 09:17:31 +08:00
commit 4a2be3b4d0
1351 files changed
+56547

No files matched your search

+355
View File
@@ -0,0 +1,355 @@
import { useCallback, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Image, Title, Cursor, ExhibitFlipOverlay, SceneHint } 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 = 'scene3'
const EMPTY_SET = new Set<number>()
// 第一阶段:解锁相关资源(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/scene3/场景3展品3') },
2: { src: getStaticImage('zhangleping/reflection/scene3/场景3展品1') },
}
const exhibitDialogues: Record<ExhibitId, Record<string, string>> = {
1: {
text: '1946年,米价涨了300倍,但我还是想把最后半块饼分给更饿的人。',
audioSrc: getPublicPath('audio/zhangleping/mp3_zhangleping_26.mp3'),
},
2: {
text: '那时候我没家,但总想帮别人......爷爷说我心里还装着温暖。',
audioSrc: getPublicPath('audio/zhangleping/mp3_zhangleping_25.mp3'),
},
}
// 第二阶段:点击常量
const CORRECT_CLICK_ITEM = 'click-2'
const CLICK_ERROR_HINT =
'这些物品虽然与三毛有关,但并不是解锁记忆碎片的关键。试试找找与“苦难里还有善良”相关的物品吧!'
const PERSISTENT_CLICK_HINT = '点击正确的物品解锁记忆碎片'
const CLICK_ERROR_HINT_DURATION_MS = 3200
const CLICK_ERROR_HINT_RESET_DELAY_MS = 3300
const CLICK_ITEMS = [
{
id: 'click-1',
className: 'absolute left-[3%] top-[6%]',
src: 'zhangleping/reflection/scene3/点击项1',
},
{
id: 'click-2',
className: 'absolute left-[36.3%] bottom-[15%]',
src: 'zhangleping/reflection/scene3/点击项2',
},
{
id: 'click-3',
className: 'absolute left-180 bottom-10',
src: 'zhangleping/reflection/scene3/点击项3',
},
] as const
const ZhanglepingReflectionScene3 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
const { markReflectionCursorVisited } = useZhanglepingStore()
const visited = useZhanglepingStore((state) => state.visitedReflectionCursors[SCENE] ?? EMPTY_SET)
// 第一阶段
const [activeExhibit, setActiveExhibit] = useState<ExhibitId | null>(null)
const [showDialogue, setShowDialogue] = useState(false)
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: 1, dependsOn: [2] },
{ id: 2, initialEnabled: true },
],
visited,
})
// 第二阶段:点击物品
const isClickPhase = 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(isClickPhase, PERSISTENT_CLICK_HINT)
const handleClickItem = useCallback(
(itemId: string) => {
if (!isClickPhase) return
if (itemId !== CORRECT_CLICK_ITEM) {
hint.showTempHint(
CLICK_ERROR_HINT,
CLICK_ERROR_HINT_DURATION_MS,
CLICK_ERROR_HINT_RESET_DELAY_MS
)
return
}
// 正确:弹出展品与对话(碎片在关闭碎片展品窗口时才解锁)
hint.clearAndReset()
setShowItemExhibit(true)
},
[hint, isClickPhase]
)
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"
style={{
backgroundImage: getBackgroundImage(
`${STAR_NAME}/reflection/scene3/${isClickPhase ? '场景-点击' : '场景-进入'}`
),
}}
>
<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: -20 }}>
苦难善良·记忆碎片已解锁
</SceneHint>
)}
{/* 场景提示:点击阶段常驻/错误提示 */}
{!showItemExhibit &&
!showFragmentOverlay &&
!fragmentCompleted &&
isClickPhase &&
hint.hintMode && (
<SceneHint
when
triggerKey={hint.hintTriggerKey}
offset={{ x: -20 }}
durationMs={hint.hintMode === 'temp' ? hint.hintDurationMs : undefined}
persistent={hint.hintMode === 'persistent' ? true : undefined}
>
{hint.hintText}
</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))'
}
}}
onMouseLeave={(e) => {
if (isClickPhase) {
e.currentTarget.style.filter = 'none'
}
}}
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"
size={0.6}
disabled={!isEnabled(1)}
read={visited.has(1)}
onClick={() => openExhibit(1)}
/>
<Cursor
className="absolute left-254 bottom-68"
size={0.6}
disabled={!isEnabled(2)}
read={visited.has(2)}
onClick={() => openExhibit(2)}
/>
</>
)}
{/* 记忆碎片区域(第二阶段及之后显示) */}
{(isClickPhase || fragmentCompleted) && (
<div
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'
)}
style={{ backgroundImage: getBackgroundImage('zhangleping/reflection/clue/泡泡') }}
>
<Image
style={
fragmentCompleted
? undefined
: {
filter: 'grayscale(100%) brightness(0)',
opacity: 0.7,
}
}
src={getStaticImage('zhangleping/reflection/clue/碎片3')}
/>
</div>
)}
{/* 离开场景按钮(碎片展品关闭后出现) */}
{fragmentCompleted && !showItemExhibit && !showFragmentOverlay && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage('components/place/离开场景')}
button
onClick={handleLeave}
/>
)}
{/* 第一阶段:展品弹层 */}
{activeExhibit && (
<ExhibitFlipOverlay
src={exhibitAssets[activeExhibit].src}
visible
onEnterComplete={() => setShowDialogue(true)}
onClose={closeExhibit}
/>
)}
{/* 第一阶段:展品对话 */}
{activeExhibit && (
<SanmaoDialogueSteps
showMask={false}
visible={showDialogue}
onVisibleChange={setShowDialogue}
audioChannelId={`zhangleping-exhibit-s3-${activeExhibit}`}
zIndex={10000}
>
<SanmaoDialogueItem audioSrc={exhibitDialogues[activeExhibit].audioSrc}>
{exhibitDialogues[activeExhibit].text}
</SanmaoDialogueItem>
</SanmaoDialogueSteps>
)}
{/* 第二阶段:点击物品2后的展品弹层(场景3展品2) */}
<ExhibitFlipOverlay
src={getStaticImage('zhangleping/reflection/scene3/场景3展品2')}
visible={showItemExhibit}
onEnterComplete={() => setShowItemDialogue(true)}
onClose={handleCloseItemExhibit}
/>
{/* 第二阶段:展品对话 */}
<SanmaoDialogueSteps
showMask={false}
visible={showItemDialogue}
onVisibleChange={setShowItemDialogue}
audioChannelId="zhangleping-exhibit-s3-item2"
zIndex={10000}
>
<SanmaoDialogueItem audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_27.mp3')}>
流浪的三毛,从来没忘记帮助别人,苦难里还有善良!
</SanmaoDialogueItem>
</SanmaoDialogueSteps>
{/* 第二阶段:碎片展品(碎片3详情) */}
<ExhibitFlipOverlay
src={getStaticImage('zhangleping/reflection/scene3/碎片3详情')}
visible={showFragmentOverlay}
onClose={handleCloseFragmentOverlay}
/>
</div>
)
}
export default ZhanglepingReflectionScene3