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

+49 -13
View File
@@ -4,7 +4,8 @@ import { useNavigate, useParams } from 'react-router-dom'
import { Image } from '@/components'
import { usePuzzleStore } from '@/store/puzzleStore'
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
import { getUserDefine } from '@/lib/utils/api-common'
import { getUserDefineResult } from '@/lib/utils/api-common'
import { showYibanSyncErrorPrompt, showYibanUnauthenticatedPrompt } from '@/components'
import {
useHuangyanpeiStore,
HUANGYANPEI_LESSON_ID,
@@ -37,28 +38,63 @@ const Huangyanpei = () => {
useEffect(() => {
;(async () => {
try {
const raw = await getUserDefine(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
if (!raw) return
const result = await getUserDefineResult(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
const data = JSON.parse(raw) as {
if (result.status !== 'success') {
useHuangyanpeiStore.setState((prev) => ({
...prev,
visitedExhibits: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
shownReflectionCompletionGuides: {},
reflectionScene2Choice: null,
}))
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
reflectionRoute?: string
shownReflectionCompletionGuides?: Record<string, boolean>
reflectionScene2Choice?: '铁工科' | '商科' | null
}
const validReflectionRoute =
data.reflectionRoute &&
['reflection', 'reflection2', 'reflection3', 'reflection4'].includes(data.reflectionRoute)
? data.reflectionRoute
: 'reflection'
useHuangyanpeiStore.setState((prev) => ({
...prev,
lastVisitedPlace:
data.lastVisitedPlace !== undefined ? data.lastVisitedPlace : prev.lastVisitedPlace,
reflectionRoute:
data.reflectionRoute &&
['reflection', 'reflection2', 'reflection3', 'reflection4'].includes(
data.reflectionRoute
)
? data.reflectionRoute
: (prev.reflectionRoute ?? 'reflection'),
visitedExhibits: Object.fromEntries(
Object.entries(data.visitedExhibits ?? {}).map(([place, exhibits]) => [
place,
new Set(Array.isArray(exhibits) ? exhibits : []),
])
),
lastVisitedPlace: data.lastVisitedPlace ?? null,
reflectionRoute: validReflectionRoute,
shownReflectionCompletionGuides: data.shownReflectionCompletionGuides ?? {},
reflectionScene2Choice: data.reflectionScene2Choice ?? null,
}))
} catch (error) {
useHuangyanpeiStore.setState((prev) => ({
...prev,
visitedExhibits: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
shownReflectionCompletionGuides: {},
reflectionScene2Choice: null,
}))
showYibanSyncErrorPrompt()
console.error('Failed to load Huangyanpei store', error)
}
})()
+1 -1
View File
@@ -546,7 +546,7 @@ const HuangyanpeiInternalization = () => {
onContinue={() => {
setShowStudyGuide(false)
setCourseTimeProgress(12)
navigate('/menu')
navigate('/learning-map', { state: { completedResidence: 'huangyanpei' } })
}}
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_31.mp3')}
/>
+1 -1
View File
@@ -345,7 +345,7 @@ const HuangyanpeiPlace3 = () => {
speaker="student"
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_07.mp3')}
>
这所川沙小学堂就是黄炎培先生“教育救国”的第一个试验田吧?我们现在学习的课程有专业课、实习实训课都提倡技能培养,当年他是不是也很注重动手能力呀?
这所川沙小学堂就是黄炎培先生“教育救国”的第一个试验田吧?我们现在的专业课、实训课都提倡技能培养,当年他是不是也很注重动手能力呀?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
+1 -1
View File
@@ -342,7 +342,7 @@ const HuangyanpeiPlace4 = () => {
{/* 题目 */}
<SingleChoiceQuizPanel
visible={showQuiz}
title={`【单选】中华职业学校首批招生78人,凭借____口号和“免学费、包分配”的优待,开创了中国近代职业教育的先河。`}
title={`【单选】中华职业学校首批招生78人,凭借____口号和“免学费、包分配”的优待,开创了中国近代职业教育的先河。`}
options={[
{ value: 'A', label: 'A. 学而优则仕' },
{ value: 'B', label: 'B. 双手万能、手脑并用' },
+2 -2
View File
@@ -150,13 +150,13 @@ const HuangyanpeiPlace6 = () => {
speaker="guide"
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_2.mp3')}
>
第一次把”中等专业学校”正式纳入国家学制,让职业教育有了明确的国家定位。1952年,他又推动建立了上海机械学校、南京航空学校等五所中专,这些学校为国家”一五”计划输送了6.3万名技术工人,为工业发展提供了坚实的人才支撑。
第一次把“中等专业学校”正式纳入国家学制,让职业教育有了明确的国家定位。1952年,他又推动建立了上海机械学校、南京航空学校等五所中专,这些学校为国家“一五”计划输送了6.3万名技术工人,为工业发展提供了坚实的人才支撑。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_3.mp3')}
>
黄炎培先生曾说过:”我要把最后一滴血,滴在职业教育的讲台上。”他用一生践行了这句话,而我们现在中职教育强调的”工学结合、德技并修”,正是对他”手脑并用、注重实践”理念的传承和发展。
黄炎培先生曾说过:“我要把最后一滴血,滴在职业教育的讲台上。”他用一生践行了这句话,而我们现在中职教育强调的“工学结合、德技并修”,正是对他“手脑并用、注重实践”理念的传承和发展。
</GuideStepsItem>
</GuideSteps>
+2 -2
View File
@@ -234,7 +234,7 @@ const HuangyanpeiReflectionScene2 = () => {
<SuccessAlert
visible={showQuizSuccess}
title=""
content="恭喜你!选择正确。不愧是从川沙小学堂走出来!"
content="恭喜你!选择正确。不愧是从川沙小学堂走出来!"
onClose={() => {
setShowQuizSuccess(false)
}}
@@ -246,7 +246,7 @@ const HuangyanpeiReflectionScene2 = () => {
onContinue={handleCompleteGuideContinue}
verticalAlign="center"
>
第二关完成!“职业方向锁定!”去新学校了解你的专业吧。
第二关完成!职业方向锁定!去新学校了解你的专业吧。
</StudyGuideModal>
</div>
)
@@ -175,7 +175,7 @@ const ChoiceDialog2 = ({ visible = true, onClose, onSelect }: ChoiceDialog2Props
<SuccessAlert
visible={showSuccessAlert}
title="选得好!"
content="不管是做工匠还是做买卖,学好本事就能报效国家,这个选择会影响你在中华职业学校的专业哦!"
content="不管是做工匠还是做买卖,学好本事就能报效国家,这个选择会影响你在中华职业学校的专业哦!"
onClose={handleSuccessAlertContinue}
/>
</div>
@@ -114,7 +114,7 @@ const Scene2QuizDialog = ({ visible = true, onClose, onComplete }: Scene2QuizDia
<div className="flex-1">
<div className="text-5xl font-bold mb-6 -ml-[26px]">【单选】</div>
<div className="text-4xl leading-relaxed">
杨斯盛先生近期监工浦东中学建设,有一份上月的工人工钱支出表需要核对,你来告诉我答案吧?
杨斯盛先生近期监工浦东中学建设,有一份上月的工人工钱支出表需要核对,你来告诉我答案吧!
</div>
</div>
@@ -11,6 +11,7 @@ import { useHuangyanpeiStore } from '@/store/huangyanpeiStore'
import { setCourseTimeProgress } from '@/lib/utils/api-common'
import { useCursorController } from '@/hooks/useCursorController'
import { saveHuangyanpeiStore } from '@/store/huangyanpeiStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
const starName = 'huangyanpei'
@@ -20,6 +21,8 @@ const HuangyanpeiReflectionScene3 = () => {
moduleName: string
}>()
const { triggerPuzzleAnimation } = usePuzzleAnimation()
//喇叭考题弹窗
const [showQuizDialog, setShowQuizDialog] = useState(false)
@@ -252,6 +255,7 @@ const HuangyanpeiReflectionScene3 = () => {
onComplete={() => {
setShowQuizDialog(false)
markReflectionCursorVisited(scene, 3)
triggerPuzzleAnimation(15)
}}
/>
</div>
+2 -3
View File
@@ -8,10 +8,9 @@ import TowelSalesDialog from './scene4/TowelSalesDialog'
import type { VideoRef } from '@/components/video'
import { QuizDialog } from './components'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
import { useHuangyanpeiStore } from '@/store/huangyanpeiStore'
import { useHuangyanpeiStore, saveHuangyanpeiStore } from '@/store/huangyanpeiStore'
import { setCourseTimeProgress } from '@/lib/utils/api-common'
import { useCursorController } from '@/hooks/useCursorController'
import { saveHuangyanpeiStore } from '@/store/huangyanpeiStore'
const starName = 'huangyanpei'
@@ -186,7 +185,6 @@ const HuangyanpeiReflectionScene4 = () => {
onComplete={() => {
// 完成拖拽后更新visited状态
markReflectionCursorVisited(scene, 2)
triggerPuzzleAnimation(15)
}}
/>
@@ -238,6 +236,7 @@ const HuangyanpeiReflectionScene4 = () => {
onComplete={() => {
setShowQuizDialog(false)
markReflectionCursorVisited(scene, 3)
triggerPuzzleAnimation(15)
}}
/>
@@ -91,19 +91,19 @@ const TowelSalesDialog = ({ visible = true, onClose, onComplete }: TowelSalesDia
() => [
{
value: 'option1',
label: 'A.这是中华职校生产的国货,质量比洋货还好!',
label: 'A.这是中华职校生产的国货,质量比洋货还好!',
},
{
value: 'option2',
label: 'B.价格便宜,量大从优,诚信经营绝不掺假',
label: 'B.价格便宜,量大从优,诚信经营绝不掺假。',
},
{
value: 'option3',
label: 'C.买国货就是支持实业救国,为国家出一份力!',
label: 'C.买国货就是支持实业救国,为国家出一份力!',
},
{
value: 'option4',
label: 'D.我只管赚钱,东西好不好不管哦',
label: 'D.我只管赚钱,东西好不好不管哦。',
},
],
[]