fix: 修复开发/测试包模拟进度被易班同步覆盖的问题

进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖;
现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度,
确保测试包的"到达某故居/全流程完成"调试入口生效。

同时纳入近期未提交的页面、组件、资源与环境变量改动。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-23 10:19:36 +08:00
1 parent 6d40e19346
commit 49ecabbf3a
155 files changed
+1956 -724

No files matched your search

+76 -1
View File
@@ -4,7 +4,24 @@ import { Image } from '@/components'
import { usePuzzleStore } from '@/store/puzzleStore'
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
import { useNavigate, useParams } from 'react-router-dom'
import { useRongdeshengStore } from '@/store/rongdeshengStore'
import { getUserDefineResult } from '@/lib/utils/api-common'
import { showYibanSyncErrorPrompt, showYibanUnauthenticatedPrompt } from '@/components'
import {
RONGDESHENG_KEY,
RONGDESHENG_LESSON_ID,
useRongdeshengStore,
} from '@/store/rongdeshengStore'
const deserializeVisitedExhibits = (
data: Record<string, number[]> | undefined
): Record<string, Set<number>> => {
if (!data || typeof data !== 'object') return {}
const visitedExhibits: Record<string, Set<number>> = {}
for (const [place, exhibits] of Object.entries(data)) {
visitedExhibits[place] = new Set(Array.isArray(exhibits) ? exhibits : [])
}
return visitedExhibits
}
const RongdeshengHome = () => {
const navigate = useNavigate()
@@ -25,6 +42,64 @@ const RongdeshengHome = () => {
}
}, [isLoaded, navigate, puzzles, starName])
useEffect(() => {
;(async () => {
try {
const result = await getUserDefineResult(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY)
if (result.status !== 'success') {
useRongdeshengStore.setState((prev) => ({
...prev,
visitedExhibits: {},
lastVisitedPlace: null,
reflectionCompleted: {},
reflectionVideoViewed: {},
reflectionFinalCompleted: false,
reflectionGuideShown: false,
}))
if (result.status === 'unauthenticated') {
showYibanUnauthenticatedPrompt()
} else if (result.status === 'error') {
showYibanSyncErrorPrompt()
}
return
}
const data = JSON.parse(result.content) as {
visitedExhibits?: Record<string, number[]>
lastVisitedPlace?: string | null
reflectionCompleted?: Record<string, boolean>
reflectionVideoViewed?: Record<string, Record<string, boolean>>
reflectionFinalCompleted?: boolean
reflectionGuideShown?: boolean
}
useRongdeshengStore.setState((prev) => ({
...prev,
visitedExhibits: deserializeVisitedExhibits(data.visitedExhibits),
lastVisitedPlace: data.lastVisitedPlace ?? null,
reflectionCompleted: data.reflectionCompleted ?? {},
reflectionVideoViewed: data.reflectionVideoViewed ?? {},
reflectionFinalCompleted: data.reflectionFinalCompleted ?? false,
reflectionGuideShown: data.reflectionGuideShown ?? false,
}))
} catch (error) {
useRongdeshengStore.setState((prev) => ({
...prev,
visitedExhibits: {},
lastVisitedPlace: null,
reflectionCompleted: {},
reflectionVideoViewed: {},
reflectionFinalCompleted: false,
reflectionGuideShown: false,
}))
showYibanSyncErrorPrompt()
console.error('Failed to load Rongdesheng store', error)
}
})()
}, [])
if (!moduleName) return null
return (
+2 -2
View File
@@ -47,7 +47,7 @@ type QuizAnchor = {
const QUIZ_ANCHORS: QuizAnchor[] = [
{
id: 'quiz-1',
time: 60,
time: 60.5,
type: 'drag',
title: '',
prompt: '【拖拽】荣德生大力发展与国计民生密切相关的',
@@ -505,7 +505,7 @@ const RongdeshengInternalization = () => {
onContinue={() => {
setShowEnding(false)
setCourseTimeProgress(21)
navigate('/menu')
navigate('/learning-map', { state: { completedResidence: 'rongdesheng' } })
}}
/>
</div>
+1 -1
View File
@@ -262,7 +262,7 @@ const RongdeshengPlace2 = () => {
speaker="student"
audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_07.mp3')}
>
哇,这是福新面粉公司的老照片!荣德生先生为什么选择办面粉,面粉厂又为什么在上海创办?福新面粉厂后来发展得怎么样?
哇,这是福新面粉公司的老照片!荣德生先生为什么选择办面粉厂,面粉厂又为什么在上海创办?福新面粉厂后来发展得怎么样?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
+1 -3
View File
@@ -186,8 +186,6 @@ const RongdeshengPlace4 = () => {
// 触发碎片获取动画(展品3 -> 碎片13,展品4 -> 碎片14)
if (closingId === 3) {
triggerPuzzleAnimation(13)
} else if (closingId === 4) {
triggerPuzzleAnimation(14)
}
if (closingId === 4) {
@@ -377,7 +375,7 @@ const RongdeshengPlace4 = () => {
<EndingOverlay
visible={endingGuideVisible}
audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_30.mp3')}
text="接下来,请你跨越时空,走进1915年的申新纺织厂,参与工厂的创办与运营中,在模拟决策中感受民族工业在风雨中崛起的力量。让我们带着这份感动与启发,传承爱国情怀,续写新时代的实业篇章。"
text="接下来,请你跨越时空,走进1915年的申新纺织厂,参与工厂的创办与运营,在模拟决策中感受民族工业在风雨中崛起的力量。让我们带着这份感动与启发,传承爱国情怀,续写新时代的实业篇章。"
animationSpeed={200}
onContinue={() => setEndingGuideVisible(false)}
/>
@@ -32,12 +32,17 @@ export const FinalScene = ({
{finalVideoStage !== 'idle' && (
<div className="fixed inset-0 z-50 w-full h-full bg-black">
{/* 返回按钮 */}
<div className="bg-[#5C524B] absolute right-6 top-6 z-10">
<Image src={getStaticImage('components/离开')} button onClick={onExit} />
</div>
<Video src={FINAL_VIDEO_SRC} autoPlay onEnded={onVideoEnded} className="w-full h-full" />
{finalVideoStage === 'ended' && (
<div
className="absolute w-[844px] h-[188px] bottom-5 right-160 bg-cover bg-center bg-no-repeat cursor-pointer"
className="absolute w-[844px] h-[188px] bottom-5 right-160 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: getBackgroundImage('rongdesheng/reflection/提示版') }}
onClick={onExit}
// onClick={onExit}
>
<div className="absolute inset-0 flex flex-col items-center justify-center text-2xl font-normal p-4">
<div
@@ -48,7 +48,7 @@ export const QuestionPanel = ({
}}
onClick={() => onOptionSelect(option.id)}
>
<div className="flex justify-center items-center h-full relative">
<div className="flex justify-start items-center h-full relative pl-5">
<span className="text-[#6D6552] text-2xl">{option.text}</span>
<div className="absolute -left-8 -top-6">
{isSelected && option.isCorrect && (
@@ -1,9 +1,10 @@
export const HINT_TEXTS = {
answeredPending: '选择成功,在场景中点击对应气泡查看决策影响',
answeredAllViewed: '已了解所有决策影响,请点击提交查看结果',
answeredPending: '选择成功,在场景中点击对应气泡查看决策影响。',
answeredAllViewed: '已了解所有决策影响,请点击提交查看结果。',
partialCorrect: '还有合适的决策未选择,请继续补充。',
finalScene: '恭喜你成功闯过所有难关!点击合成按钮,解锁申新纺织厂从初创到自强的完整红色故事吧~',
finalEnded: '恭喜你完成推演!保存勋章,和同学分享申新纺织厂从初创到自强的红色实业故事吧~',
finalScene:
'恭喜你成功闯过所有难关!点击“合成坚守图谱”按钮,解锁申新纺织厂从初创到自强的完整红色故事吧~',
finalEnded: '恭喜你完成推演!和同学分享申新纺织厂从初创到自强的红色实业故事吧~',
} as const
export type HintKey = keyof typeof HINT_TEXTS
+6 -1
View File
@@ -13,11 +13,13 @@ import { HINT_AUDIOS, HINT_TEXTS } from './constants'
import { MODULES, getSceneTitle } from './modules.data'
import type { ModuleId, ModuleState, VideoPlayingState } from './types'
import { setCourseTimeProgress } from '@/lib/utils/api-common'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
const RongdeshengReflection = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
const starName = 'rongdesheng'
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 持久化 store
const persistedCompleted = useRongdeshengStore((s) => s.reflectionCompleted)
@@ -223,6 +225,9 @@ const RongdeshengReflection = () => {
[currentModuleId]: { ...prev[currentModuleId], completed: true },
}))
markReflectionCompleted(currentModuleId)
if (currentModuleId === 'scene3') {
triggerPuzzleAnimation(14)
}
}
// 结算页"下一场景":切到下一场景或进入最终合成场景
@@ -296,7 +301,7 @@ const RongdeshengReflection = () => {
{/* 返回按钮 */}
<Image
className="absolute right-5 top-5 z-10"
src={getStaticImage('components/返回')}
src={getStaticImage('components/离开')}
button
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
/>
@@ -11,17 +11,17 @@ export const MODULES: Module[] = [
options: [
{
id: 'opt1-1',
text: '选址上海杨树浦(临黄浦江,便于原料运输)',
text: 'A.选址上海杨树浦(临黄浦江,便于原料运输)',
isCorrect: true,
videoResourceId: 'video1-1',
},
{
id: 'opt1-2',
text: '联合无锡帮华商与实业同仁招股筹款',
text: 'B.联合无锡帮华商与实业同仁招股筹款',
isCorrect: true,
videoResourceId: 'video1-2',
},
{ id: 'opt1-3', text: '依附租界洋行,换取建厂便利与资金支持', isCorrect: false },
{ id: 'opt1-3', text: 'C.依附租界洋行,换取建厂便利与资金支持', isCorrect: false },
],
videoResources: [
{
@@ -86,7 +86,7 @@ export const MODULES: Module[] = [
'护厂抗争的胜利,是民族工业坚守爱国初心的体现,也是国人联合“反抗压迫、团结抗争”的印证!',
resultImage: 'rongdesheng/reflection/场景2结算',
wrongHint:
'荣氏兄弟坚守"实业救国"初心,拒绝洋行干涉企业经营,不愿民族产业沦为外资附庸!试试其他方案吧~',
'荣氏兄弟坚守“实业救国”初心,拒绝洋行干涉企业经营,不愿民族产业沦为外资附庸!试试其他方案吧~',
audio: { wrongHint: '' },
},
{
@@ -181,7 +181,8 @@ export const MODULES: Module[] = [
{
id: 'opt5-1',
text: 'A.仿制外国先进设备,改良生产工艺',
isCorrect: false,
videoResourceId: 'video5-2',
isCorrect: true,
},
{
id: 'opt5-2',
@@ -192,8 +193,7 @@ export const MODULES: Module[] = [
{
id: 'opt5-3',
text: 'C.降价促销,以低价抢占市场',
videoResourceId: 'video5-2',
isCorrect: true,
isCorrect: false,
},
],
videoResources: [