更新依赖项及历史人物相关内容资源

- 更新 package.json 和 package-lock.json 中的依赖项
- 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件
- 新增场景和组件的图片资源
- 清理未使用的音频和图片文件
- 优化多个组件和页面的实现逻辑
This commit is contained in:
zhongyidian committed 2026-04-20 13:27:18 +08:00
1 parent b1d55b9464
commit e0fa47d21a
629 files changed
+24390 -2991

No files matched your search

+2 -2
View File
@@ -154,8 +154,8 @@ export const useChenyunStore = create<ChenyunStore>()(
)
// ========== 易班平台数据保存 ==========
const CHENYUN_LESSON_ID = 'Cheny_duihua'
const CHENYUN_KEY = 22
export const CHENYUN_LESSON_ID = 'Cheny_duihua'
export const CHENYUN_KEY = 22
export const saveChenyunStore = (): void => {
const state = useChenyunStore.getState()
-81
View File
@@ -1,81 +0,0 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
export interface Clue {
id: string // 格式: "sunzhongshan/reflection/scene1/clue1"
name: string // 线索名称,如 "线索1"
collected: boolean // 是否已收集
collectedAt?: number // 收集时间戳
}
interface ClueStore {
clues: Clue[]
// 收集线索
collectClue: (id: string, name: string) => void
// 检查线索是否已收集
isClueCollected: (id: string) => boolean
// 获取指定场景的所有线索
getSceneClues: (sceneId: string) => Clue[]
// 重置指定场景的线索
resetSceneClues: (sceneId: string) => void
// 重置所有线索
resetAll: () => void
}
export const useClueStore = create<ClueStore>()(
persist(
(set, get) => ({
clues: [],
collectClue: (id: string, name: string) => {
const { clues } = get()
const existingClue = clues.find((c) => c.id === id)
if (!existingClue) {
set({
clues: [
...clues,
{
id,
name,
collected: true,
collectedAt: Date.now(),
},
],
})
} else if (!existingClue.collected) {
set({
clues: clues.map((c) =>
c.id === id ? { ...c, collected: true, collectedAt: Date.now() } : c
),
})
}
},
isClueCollected: (id: string) => {
const { clues } = get()
return clues.some((c) => c.id === id && c.collected)
},
getSceneClues: (sceneId: string) => {
const { clues } = get()
return clues.filter((c) => c.id.startsWith(sceneId))
},
resetSceneClues: (sceneId: string) => {
const { clues } = get()
set({
clues: clues.filter((c) => !c.id.startsWith(sceneId)),
})
},
resetAll: () => {
set({ clues: [] })
},
}),
{
name: STORE_NAMES.CLUE, // localStorage key
}
)
)
+108
View File
@@ -1,12 +1,30 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine, getUserDefine } from '@/lib/utils/api-common'
interface HuangyanpeiStore {
// 存储每个场景(place)的已访问展品ID集合
// 格式:{ place1: Set<1, 2>, place2: Set<1, 2>, ... }
visitedExhibits: Record<string, Set<number>>
// 体悟模块“进入下一场景”引导弹窗是否已展示
shownReflectionCompletionGuides: Record<string, boolean>
markReflectionCompletionGuideShown: (scene: string) => void
hasShownReflectionCompletionGuide: (scene: string) => boolean
// scene2 的专业选择缓存
reflectionScene2Choice: '铁工科' | '商科' | null
setReflectionScene2Choice: (choice: '铁工科' | '商科' | null) => void
// 上次访问的对话场景(place1~place6)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// 体悟模块当前路由(reflection / reflection2 / reflection3 / reflection4)
reflectionRoute: string
setReflectionRoute: (route: string) => void
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
@@ -76,6 +94,10 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
shownReflectionCompletionGuides: {},
reflectionScene2Choice: null,
lastVisitedPlace: null,
reflectionRoute: 'reflection',
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
@@ -113,6 +135,20 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
set({ visitedExhibits: {} })
},
markReflectionCompletionGuideShown: (scene: string) => {
set((state) => ({
shownReflectionCompletionGuides: {
...state.shownReflectionCompletionGuides,
[scene]: true,
},
}))
},
hasShownReflectionCompletionGuide: (scene: string) => {
const { shownReflectionCompletionGuides } = get()
return shownReflectionCompletionGuides[scene] || false
},
// ========== 体悟模块状态 ==========
visitedReflectionCursors: {},
@@ -151,12 +187,20 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
resetAllReflectionCursors: () => {
set({ visitedReflectionCursors: {} })
},
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
setReflectionScene2Choice: (choice) => set({ reflectionScene2Choice: choice }),
}),
{
name: STORE_NAMES.HUANGYANPEI, // localStorage key
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
visitedReflectionCursors: serializeVisitedExhibits(state.visitedReflectionCursors),
shownReflectionCompletionGuides: state.shownReflectionCompletionGuides,
reflectionScene2Choice: state.reflectionScene2Choice,
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
}),
onRehydrateStorage: () => (state) => {
if (state) {
@@ -165,8 +209,72 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
state.visitedReflectionCursors = deserializeVisitedExhibits(
state.visitedReflectionCursors as any
)
if ((state as any).shownReflectionCompletionGuides === undefined) {
;(state as any).shownReflectionCompletionGuides = {}
}
if ((state as any).reflectionScene2Choice === undefined) {
;(state as any).reflectionScene2Choice = null
}
if ((state as any).lastVisitedPlace === undefined) {
;(state as any).lastVisitedPlace = null
}
if ((state as any).reflectionRoute === undefined) {
;(state as any).reflectionRoute = 'reflection'
}
}
},
}
)
)
// ========== 易班平台数据保存(对话 + 体悟模块) ==========
export const HUANGYANPEI_LESSON_ID = 'Huangyp_duihua'
export const HUANGYANPEI_KEY = 10
export const saveHuangyanpeiStore = (): void => {
const state = useHuangyanpeiStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
shownReflectionCompletionGuides: state.shownReflectionCompletionGuides,
reflectionScene2Choice: state.reflectionScene2Choice,
}
try {
setUserDefine(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Huangyanpei store', error)
}
}
export const loadHuangyanpeiStore = async (): Promise<void> => {
try {
const raw = await getUserDefine(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
if (!raw) {
return
}
const payload = JSON.parse(raw) as {
visitedExhibits?: Record<string, number[]>
lastVisitedPlace?: string | null
reflectionRoute?: string
shownReflectionCompletionGuides?: Record<string, boolean>
reflectionScene2Choice?: '铁工科' | '商科' | null
}
useHuangyanpeiStore.setState((state) => ({
visitedExhibits: payload.visitedExhibits
? deserializeVisitedExhibits(payload.visitedExhibits)
: state.visitedExhibits,
lastVisitedPlace: payload.lastVisitedPlace ?? state.lastVisitedPlace,
reflectionRoute: payload.reflectionRoute ?? state.reflectionRoute,
shownReflectionCompletionGuides:
payload.shownReflectionCompletionGuides ?? state.shownReflectionCompletionGuides,
reflectionScene2Choice: payload.reflectionScene2Choice ?? state.reflectionScene2Choice,
}))
} catch (error) {
console.error('Failed to load Huangyanpei store', error)
}
}
+2 -1
View File
@@ -1,8 +1,9 @@
export { useChenyunStore } from './chenyunStore'
export { useHuangyanpeiStore } from './huangyanpeiStore'
export { useLuxunStore } from './luxunStore'
export { useNieerStore } from './nieerStore'
export { useSunzhongshanStore } from './sunzhongshanStore'
export { useClueStore } from './clueStore'
export { useZhanglepingStore } from './zhanglepingStore'
export { usePuzzleStore } from './puzzleStore'
export { useUserStore } from './userStore'
export { STORE_NAMES } from './storeNames'
+42
View File
@@ -1,12 +1,21 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
interface LuxunStore {
// 存储每个场景(place)的已访问展品ID集合
// 格式:{ place1: Set<1, 2>, place2: Set<1, 2>, ... }
visitedExhibits: Record<string, Set<number>>
// 体悟模块当前路由(reflection / reflection2 / reflection3)
reflectionRoute: string
setReflectionRoute: (route: string) => void
// 上次访问的场景(place1~place4)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
@@ -56,6 +65,8 @@ export const useLuxunStore = create<LuxunStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
reflectionRoute: 'reflection',
lastVisitedPlace: null,
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
@@ -92,17 +103,48 @@ export const useLuxunStore = create<LuxunStore>()(
resetAll: () => {
set({ visitedExhibits: {} })
},
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
}),
{
name: STORE_NAMES.LUXUN,
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
}),
onRehydrateStorage: () => (state) => {
if (state) {
state.visitedExhibits = deserializeVisitedExhibits(state.visitedExhibits as any)
if (state.lastVisitedPlace === undefined) {
state.lastVisitedPlace = null
}
if ((state as any).reflectionRoute === undefined) {
;(state as any).reflectionRoute = 'reflection'
}
}
},
}
)
)
// ========== 易班平台数据保存(对话模块) ==========
export const LUXUN_LESSON_ID = 'Lux_duihua'
export const LUXUN_KEY = 7
export const saveLuxunStore = (): void => {
const state = useLuxunStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
}
try {
setUserDefine(LUXUN_LESSON_ID, LUXUN_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Luxun store', error)
}
}
+113
View File
@@ -0,0 +1,113 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
interface MaozedongStore {
// 每个场景(placeX)的已访问展品ID集合
visitedExhibits: Record<string, Set<number>>
// 上次访问的场景(place1~place4)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
// 检查展品是否已访问
isExhibitVisited: (place: string, exhibitId: number) => boolean
// 获取某个场景已访问的展品ID数组
getVisitedExhibits: (place: string) => number[]
// 清空某个场景
resetPlace: (place: string) => void
// 重置所有
resetAll: () => void
}
const serializeVisited = (data: Record<string, Set<number>>): Record<string, number[]> => {
const out: Record<string, number[]> = {}
for (const [k, v] of Object.entries(data)) out[k] = Array.from(v)
return out
}
const deserializeVisited = (
data: Record<string, number[]> | undefined
): Record<string, Set<number>> => {
if (!data || typeof data !== 'object') return {}
const out: Record<string, Set<number>> = {}
for (const [k, v] of Object.entries(data)) out[k] = Array.isArray(v) ? new Set(v) : new Set()
return out
}
export const useMaozedongStore = create<MaozedongStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
lastVisitedPlace: null,
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
const current = state.visitedExhibits[place] || new Set<number>()
if (current.has(exhibitId)) return state
return {
visitedExhibits: {
...state.visitedExhibits,
[place]: new Set([...current, exhibitId]),
},
}
})
},
isExhibitVisited: (place: string, exhibitId: number) => {
const { visitedExhibits } = get()
return visitedExhibits[place]?.has(exhibitId) || false
},
getVisitedExhibits: (place: string) => {
const { visitedExhibits } = get()
return Array.from(visitedExhibits[place] || [])
},
resetPlace: (place: string) => {
set((state) => {
const next = { ...state.visitedExhibits }
delete next[place]
return { visitedExhibits: next }
})
},
resetAll: () => set({ visitedExhibits: {}, lastVisitedPlace: null }),
}),
{
name: STORE_NAMES.MAOZEDONG,
partialize: (state) => ({
visitedExhibits: serializeVisited(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
}),
onRehydrateStorage: () => (state) => {
if (state) {
state.visitedExhibits = deserializeVisited(state.visitedExhibits as any)
if (state.lastVisitedPlace === undefined) state.lastVisitedPlace = null
}
},
}
)
)
// ========== 易班平台数据保存(参考陈云) ==========
export const MAOZEDONG_LESSON_ID = 'Maozd_duihua'
export const MAOZEDONG_KEY = 1
export const saveMaozedongStore = (): void => {
const state = useMaozedongStore.getState()
const payload = {
visitedExhibits: serializeVisited(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
}
try {
setUserDefine(MAOZEDONG_LESSON_ID, MAOZEDONG_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Maozedong store', error)
}
}
+227
View File
@@ -0,0 +1,227 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
interface NieerStore {
// 存储每个场景(place)的已访问展品ID集合
// 格式:{ place1: Set<1,2>, place2: Set<1,2>, ... }
visitedExhibits: Record<string, Set<number>>
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
// 检查展品是否已访问
isExhibitVisited: (place: string, exhibitId: number) => boolean
// 获取某个场景已访问的展品ID数组
getVisitedExhibits: (place: string) => number[]
// 重置某个场景的访问记录
resetPlace: (place: string) => void
// 重置所有访问记录
resetAll: () => void
// ========== 断点续读 ==========
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// ========== 体悟模块状态 ==========
// 存储每个reflection场景的已完成cursor ID集合
visitedReflectionCursors: Record<string, Set<number>>
// 标记reflection cursor为已完成
markReflectionCursorVisited: (scene: string, cursorId: number) => void
// 检查reflection cursor是否已完成
isReflectionCursorVisited: (scene: string, cursorId: number) => boolean
// 获取某个reflection场景已完成的cursor ID数组
getVisitedReflectionCursors: (scene: string) => number[]
// 重置某个reflection场景的cursor记录
resetReflectionScene: (scene: string) => void
// 重置所有reflection cursor记录
resetAllReflectionCursors: () => void
// ========== 旅行箱成果 ==========
// 每个 scene 对应一次分享后的成果快照(用于旅行箱展示)
reflectionWorks: Record<number, Record<string, unknown>>
setReflectionWork: (sceneId: number, payload: Record<string, unknown>) => void
resetReflectionWorks: () => void
}
// 将 Set 转换为数组以便序列化
const serializeVisitedExhibits = (
visitedExhibits: Record<string, Set<number>>
): Record<string, number[]> => {
const serialized: Record<string, number[]> = {}
for (const [place, exhibits] of Object.entries(visitedExhibits)) {
serialized[place] = Array.from(exhibits)
}
return serialized
}
// 将数组转换回 Set
const deserializeVisitedExhibits = (
serialized: Record<string, number[]> | undefined
): Record<string, Set<number>> => {
if (!serialized || typeof serialized !== 'object') {
return {}
}
const visitedExhibits: Record<string, Set<number>> = {}
for (const [place, exhibits] of Object.entries(serialized)) {
if (Array.isArray(exhibits)) {
visitedExhibits[place] = new Set(exhibits)
} else {
visitedExhibits[place] = new Set()
}
}
return visitedExhibits
}
export const useNieerStore = create<NieerStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
const currentExhibits = state.visitedExhibits[place] || new Set<number>()
if (!currentExhibits.has(exhibitId)) {
const newExhibits = {
...state.visitedExhibits,
[place]: new Set([...currentExhibits, exhibitId]),
}
return { visitedExhibits: newExhibits }
}
return state
})
},
isExhibitVisited: (place: string, exhibitId: number) => {
const { visitedExhibits } = get()
return visitedExhibits[place]?.has(exhibitId) || false
},
getVisitedExhibits: (place: string) => {
const { visitedExhibits } = get()
return Array.from(visitedExhibits[place] || [])
},
resetPlace: (place: string) => {
set((state) => {
const newExhibits = { ...state.visitedExhibits }
delete newExhibits[place]
return { visitedExhibits: newExhibits }
})
},
resetAll: () => {
set({ visitedExhibits: {} })
},
lastVisitedPlace: null,
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
// ========== 体悟模块状态 ==========
visitedReflectionCursors: {},
reflectionWorks: {},
markReflectionCursorVisited: (scene: string, cursorId: number) => {
set((state) => {
const currentCursors = state.visitedReflectionCursors[scene] || new Set<number>()
if (!currentCursors.has(cursorId)) {
const newCursors = {
...state.visitedReflectionCursors,
[scene]: new Set([...currentCursors, cursorId]),
}
return { visitedReflectionCursors: newCursors }
}
return state
})
},
isReflectionCursorVisited: (scene: string, cursorId: number) => {
const { visitedReflectionCursors } = get()
return visitedReflectionCursors[scene]?.has(cursorId) || false
},
getVisitedReflectionCursors: (scene: string) => {
const { visitedReflectionCursors } = get()
return Array.from(visitedReflectionCursors[scene] || [])
},
resetReflectionScene: (scene: string) => {
set((state) => {
const newCursors = { ...state.visitedReflectionCursors }
delete newCursors[scene]
return { visitedReflectionCursors: newCursors }
})
},
resetAllReflectionCursors: () => {
set({ visitedReflectionCursors: {} })
},
setReflectionWork: (sceneId: number, payload: Record<string, unknown>) => {
set((state) => ({
reflectionWorks: {
...state.reflectionWorks,
[sceneId]: {
...state.reflectionWorks[sceneId],
...payload,
updatedAt: Date.now(),
},
},
}))
},
resetReflectionWorks: () => {
set({ reflectionWorks: {} })
},
}),
{
name: STORE_NAMES.NIEER,
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
visitedReflectionCursors: serializeVisitedExhibits(state.visitedReflectionCursors),
lastVisitedPlace: state.lastVisitedPlace,
reflectionWorks: state.reflectionWorks,
}),
onRehydrateStorage: () => (state) => {
if (state) {
state.visitedExhibits = deserializeVisitedExhibits(state.visitedExhibits as any)
state.visitedReflectionCursors = deserializeVisitedExhibits(
state.visitedReflectionCursors as any
)
if (state.lastVisitedPlace === undefined) {
state.lastVisitedPlace = null
}
if (!state.reflectionWorks || typeof state.reflectionWorks !== 'object') {
state.reflectionWorks = {}
}
}
},
}
)
)
// ========== 易班平台数据保存(与陈云对齐) ==========
export const NIEER_LESSON_ID = 'Nieer_duihua'
export const NIEER_KEY = 5
export const saveNieerStore = (): void => {
const state = useNieerStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
}
try {
setUserDefine(NIEER_LESSON_ID, NIEER_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Nieer store', error)
}
}
+210
View File
@@ -0,0 +1,210 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
interface RongdeshengStore {
// 对话模块:每个 place 的已访问展品 ID 集合
visitedExhibits: Record<string, Set<number>>
// 断点续读:上次访问的对话模块 place(place1~place4)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// 体悟模块:每个场景的完成状态(用于解锁下一场景)
reflectionCompleted: Record<string, boolean>
// 体悟模块:每个场景已观看的视频
reflectionVideoViewed: Record<string, Record<string, boolean>>
// 体悟模块:是否已完成最终合成(看完 rongdesheng_2_11)
reflectionFinalCompleted: boolean
// 体悟模块:是否已显示过学习引导弹窗
reflectionGuideShown: boolean
// 标记某个体悟场景的视频已观看
markReflectionVideoViewed: (sceneId: string, videoId: string) => void
// 标记某个体悟场景完成
markReflectionCompleted: (sceneId: string) => void
// 标记最终合成完成
markReflectionFinalCompleted: () => void
// 标记学习引导已显示
markReflectionGuideShown: () => void
// 重置体悟进度
resetReflection: () => void
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
// 检查展品是否已访问
isExhibitVisited: (place: string, exhibitId: number) => boolean
// 获取某个场景已访问展品 ID 数组
getVisitedExhibits: (place: string) => number[]
// 重置某个场景的访问记录
resetPlace: (place: string) => void
// 重置所有访问记录
resetAll: () => void
}
const serializeVisitedExhibits = (
visitedExhibits: Record<string, Set<number>>
): Record<string, number[]> => {
const serialized: Record<string, number[]> = {}
for (const [place, exhibits] of Object.entries(visitedExhibits)) {
serialized[place] = Array.from(exhibits)
}
return serialized
}
const deserializeVisitedExhibits = (
serialized: Record<string, number[]> | undefined
): Record<string, Set<number>> => {
if (!serialized || typeof serialized !== 'object') return {}
const visitedExhibits: Record<string, Set<number>> = {}
for (const [place, exhibits] of Object.entries(serialized)) {
visitedExhibits[place] = Array.isArray(exhibits) ? new Set(exhibits) : new Set()
}
return visitedExhibits
}
export const useRongdeshengStore = create<RongdeshengStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
lastVisitedPlace: null,
reflectionCompleted: {},
reflectionVideoViewed: {},
reflectionFinalCompleted: false,
reflectionGuideShown: false,
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
markReflectionVideoViewed: (sceneId: string, videoId: string) => {
set((state) => ({
reflectionVideoViewed: {
...state.reflectionVideoViewed,
[sceneId]: {
...(state.reflectionVideoViewed[sceneId] || {}),
[videoId]: true,
},
},
}))
},
markReflectionCompleted: (sceneId: string) => {
set((state) => {
if (state.reflectionCompleted[sceneId]) return state
return {
reflectionCompleted: {
...state.reflectionCompleted,
[sceneId]: true,
},
}
})
},
markReflectionFinalCompleted: () => set({ reflectionFinalCompleted: true }),
markReflectionGuideShown: () => set({ reflectionGuideShown: true }),
resetReflection: () =>
set({
reflectionCompleted: {},
reflectionVideoViewed: {},
reflectionFinalCompleted: false,
reflectionGuideShown: false,
}),
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
const current = state.visitedExhibits[place] || new Set<number>()
if (current.has(exhibitId)) return state
return {
visitedExhibits: {
...state.visitedExhibits,
[place]: new Set([...current, exhibitId]),
},
}
})
},
isExhibitVisited: (place: string, exhibitId: number) => {
const { visitedExhibits } = get()
return visitedExhibits[place]?.has(exhibitId) || false
},
getVisitedExhibits: (place: string) => {
const { visitedExhibits } = get()
return Array.from(visitedExhibits[place] || [])
},
resetPlace: (place: string) => {
set((state) => {
const next = { ...state.visitedExhibits }
delete next[place]
return { visitedExhibits: next }
})
},
resetAll: () =>
set({
visitedExhibits: {},
lastVisitedPlace: null,
reflectionCompleted: {},
reflectionVideoViewed: {},
reflectionFinalCompleted: false,
reflectionGuideShown: false,
}),
}),
{
name: STORE_NAMES.RONGDESHENG,
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionCompleted: state.reflectionCompleted,
reflectionVideoViewed: state.reflectionVideoViewed,
reflectionFinalCompleted: state.reflectionFinalCompleted,
reflectionGuideShown: state.reflectionGuideShown,
}),
onRehydrateStorage: () => (state) => {
if (!state) return
state.visitedExhibits = deserializeVisitedExhibits(state.visitedExhibits as any)
if (state.lastVisitedPlace === undefined) state.lastVisitedPlace = null
if (!state.reflectionCompleted || typeof state.reflectionCompleted !== 'object') {
state.reflectionCompleted = {}
}
if (!state.reflectionVideoViewed || typeof state.reflectionVideoViewed !== 'object') {
state.reflectionVideoViewed = {}
}
if (typeof state.reflectionFinalCompleted !== 'boolean') {
state.reflectionFinalCompleted = false
}
if (typeof state.reflectionGuideShown !== 'boolean') {
state.reflectionGuideShown = false
}
},
}
)
)
// ========== 易班平台数据保存 ==========
export const RONGDESHENG_LESSON_ID = 'Rongds_duihua'
export const RONGDESHENG_KEY = 19
export const saveRongdeshengStore = (): void => {
const state = useRongdeshengStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionCompleted: state.reflectionCompleted,
reflectionVideoViewed: state.reflectionVideoViewed,
reflectionFinalCompleted: state.reflectionFinalCompleted,
reflectionGuideShown: state.reflectionGuideShown,
}
try {
setUserDefine(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Rongdesheng store', error)
}
}
+4
View File
@@ -5,6 +5,10 @@ export const STORE_NAMES = {
LUXUN: 'luxun-storage',
HUANGYANPEI: 'huangyanpei-storage',
CHENYUN: 'chenyun-storage',
MAOZEDONG: 'maozedong-storage',
NIEER: 'nieer-storage',
ZHANGLEPING: 'zhangleping-storage',
RONGDESHENG: 'rongdesheng-storage',
PUZZLE: 'puzzle-storage',
CLUE: 'clue-storage',
} as const
+110 -1
View File
@@ -1,6 +1,14 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
export interface Clue {
id: string
name: string
collected: boolean
collectedAt?: number
}
interface SunzhongshanStore {
// 存储每个场景(place)的已访问展品ID集合
@@ -10,6 +18,21 @@ interface SunzhongshanStore {
// 体悟模块通用布尔状态(引导是否展示、场景是否完成等)
reflectionFlags: Record<string, boolean>
// 上次访问的场景(place1~place4)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// 体悟模块当前路由(reflection、reflection1、reflection2、reflection3)
reflectionRoute: string
setReflectionRoute: (route: string) => void
// ========== 线索系统(原 clueStore,并入孙中山) ==========
clues: Clue[]
collectClue: (id: string, name: string) => void
isClueCollected: (id: string) => boolean
getSceneClues: (sceneId: string) => Clue[]
resetSceneClues: (sceneId: string) => void
// 标记展品为已访问
markExhibitVisited: (place: string, exhibitId: number) => void
@@ -66,6 +89,9 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
(set, get) => ({
visitedExhibits: {},
reflectionFlags: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
clues: [],
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
@@ -100,7 +126,13 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
},
resetAll: () => {
set({ visitedExhibits: {}, reflectionFlags: {} })
set({
visitedExhibits: {},
reflectionFlags: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
clues: [],
})
},
setReflectionFlag: (key: string, value: boolean) => {
@@ -116,12 +148,60 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
const { reflectionFlags } = get()
return !!reflectionFlags[key]
},
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
collectClue: (id: string, name: string) => {
const { clues } = get()
const existingClue = clues.find((c) => c.id === id)
if (!existingClue) {
set({
clues: [
...clues,
{
id,
name,
collected: true,
collectedAt: Date.now(),
},
],
})
} else if (!existingClue.collected) {
set({
clues: clues.map((c) =>
c.id === id ? { ...c, collected: true, collectedAt: Date.now() } : c
),
})
}
},
isClueCollected: (id: string) => {
const { clues } = get()
return clues.some((c) => c.id === id && c.collected)
},
getSceneClues: (sceneId: string) => {
const { clues } = get()
return clues.filter((c) => c.id.startsWith(sceneId))
},
resetSceneClues: (sceneId: string) => {
const { clues } = get()
set({
clues: clues.filter((c) => !c.id.startsWith(sceneId)),
})
},
}),
{
name: STORE_NAMES.SUNZHONGSHAN,
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
reflectionFlags: state.reflectionFlags,
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
clues: state.clues,
}),
onRehydrateStorage: () => (state) => {
if (state) {
@@ -129,8 +209,37 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
if (!state.reflectionFlags || typeof state.reflectionFlags !== 'object') {
state.reflectionFlags = {}
}
if (state.lastVisitedPlace === undefined) {
state.lastVisitedPlace = null
}
if (state.reflectionRoute === undefined) {
state.reflectionRoute = 'reflection'
}
if (!(state as any).clues || !Array.isArray((state as any).clues)) {
;(state as any).clues = []
}
}
},
}
)
)
// ========== 易班平台数据保存(对话模块) ==========
export const SUNZHONGSHAN_LESSON_ID = 'Sunzs_duihua'
export const SUNZHONGSHAN_KEY = 4
export const saveSunzhongshanStore = (): void => {
const state = useSunzhongshanStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
}
try {
setUserDefine(SUNZHONGSHAN_LESSON_ID, SUNZHONGSHAN_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Sunzhongshan store', error)
}
}
+199
View File
@@ -0,0 +1,199 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import { setUserDefine } from '@/lib/utils/api-common'
interface ZhanglepingStore {
// ========= 对话模块:展品访问进度/断点 =========
visitedExhibits: Record<string, Set<number>>
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
markExhibitVisited: (place: string, exhibitId: number) => void
isExhibitVisited: (place: string, exhibitId: number) => boolean
getVisitedExhibits: (place: string) => number[]
resetPlace: (place: string) => void
resetAllDialog: () => void
// 体悟模块当前路由(reflection / reflection1 / ... / reflection5)
reflectionRoute: string
setReflectionRoute: (route: string) => void
// 存储每个reflection场景的已完成cursor ID集合
// 格式:{ scene1: Set<1, 2>, scene2: Set<1, 2>, ... }
visitedReflectionCursors: Record<string, Set<number>>
// 标记reflection cursor为已完成
markReflectionCursorVisited: (scene: string, cursorId: number) => void
// 获取某个reflection场景已完成的cursor ID数组
getVisitedReflectionCursors: (scene: string) => number[]
// 绘制模块提交结果(用于在 scene5 画板处展示)
drawArtworkDataUrl: string
drawArtworkCaption: string
setDrawArtwork: (dataUrl: string, caption: string) => void
clearDrawArtwork: () => void
// 是否已经看过学习引导窗口
hasSeenStudyGuide: boolean
markStudyGuideSeen: () => void
}
// 将 Set 转换为数组以便序列化(体悟)
const serializeVisited = (visited: Record<string, Set<number>>): Record<string, number[]> => {
const serialized: Record<string, number[]> = {}
for (const [scene, ids] of Object.entries(visited)) {
serialized[scene] = Array.from(ids)
}
return serialized
}
// 将数组转换回 Set(体悟)
const deserializeVisited = (
serialized: Record<string, number[]> | undefined
): Record<string, Set<number>> => {
if (!serialized || typeof serialized !== 'object') return {}
const visited: Record<string, Set<number>> = {}
for (const [scene, ids] of Object.entries(serialized)) {
visited[scene] = new Set(Array.isArray(ids) ? ids : [])
}
return visited
}
export const useZhanglepingStore = create<ZhanglepingStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
visitedReflectionCursors: {},
drawArtworkDataUrl: '',
drawArtworkCaption: '',
hasSeenStudyGuide: false,
markExhibitVisited: (place: string, exhibitId: number) => {
set((state) => {
const current = state.visitedExhibits[place] || new Set<number>()
if (current.has(exhibitId)) return state
return {
visitedExhibits: {
...state.visitedExhibits,
[place]: new Set([...current, exhibitId]),
},
}
})
},
isExhibitVisited: (place: string, exhibitId: number) => {
const { visitedExhibits } = get()
return visitedExhibits[place]?.has(exhibitId) || false
},
getVisitedExhibits: (place: string) => {
const { visitedExhibits } = get()
return Array.from(visitedExhibits[place] || [])
},
resetPlace: (place: string) => {
set((state) => {
const next = { ...state.visitedExhibits }
delete next[place]
return { visitedExhibits: next }
})
},
resetAllDialog: () => set({ visitedExhibits: {}, lastVisitedPlace: null }),
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
markReflectionCursorVisited: (scene: string, cursorId: number) => {
set((state) => {
const current = state.visitedReflectionCursors[scene] || new Set<number>()
if (current.has(cursorId)) return state
return {
visitedReflectionCursors: {
...state.visitedReflectionCursors,
[scene]: new Set([...current, cursorId]),
},
}
})
},
getVisitedReflectionCursors: (scene: string) => {
const { visitedReflectionCursors } = get()
return Array.from(visitedReflectionCursors[scene] || [])
},
setDrawArtwork: (dataUrl: string, caption: string) =>
set({
drawArtworkDataUrl: dataUrl,
drawArtworkCaption: caption,
}),
clearDrawArtwork: () =>
set({
drawArtworkDataUrl: '',
drawArtworkCaption: '',
}),
markStudyGuideSeen: () => set({ hasSeenStudyGuide: true }),
}),
{
name: STORE_NAMES.ZHANGLEPING,
partialize: (state) => ({
visitedExhibits: serializeVisited(state.visitedExhibits),
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
visitedReflectionCursors: serializeVisited(state.visitedReflectionCursors),
drawArtworkDataUrl: state.drawArtworkDataUrl,
drawArtworkCaption: state.drawArtworkCaption,
hasSeenStudyGuide: state.hasSeenStudyGuide,
}),
onRehydrateStorage: () => (state) => {
if (!state) return
state.visitedExhibits = deserializeVisited((state as any).visitedExhibits)
if ((state as any).lastVisitedPlace === undefined) {
;(state as any).lastVisitedPlace = null
}
state.visitedReflectionCursors = deserializeVisited(state.visitedReflectionCursors as any)
if ((state as any).reflectionRoute === undefined) {
;(state as any).reflectionRoute = 'reflection'
}
if ((state as any).drawArtworkDataUrl === undefined) {
;(state as any).drawArtworkDataUrl = ''
}
if ((state as any).drawArtworkCaption === undefined) {
;(state as any).drawArtworkCaption = ''
}
if ((state as any).hasSeenStudyGuide === undefined) {
;(state as any).hasSeenStudyGuide = false
}
},
}
)
)
// ========== 易班平台数据保存(体悟模块) ==========
export const ZHANGLEPING_LESSON_ID = 'Zhanglp_tiwu'
export const ZHANGLEPING_KEY = 16
const REMOTE_MAX_DATA_URL_LENGTH = 1200
export const saveZhanglepingStore = (): void => {
const state = useZhanglepingStore.getState()
// 易班接口在当前环境是 GET 参数传输,完整 base64 图会导致 URL 超长请求失败。
// 因此远端只保存轻量摘要;完整画作仍保留在本地 persist(localStorage)中。
const remoteArtworkDataUrl =
state.drawArtworkDataUrl.length <= REMOTE_MAX_DATA_URL_LENGTH ? state.drawArtworkDataUrl : ''
const payload = {
reflectionRoute: state.reflectionRoute,
visitedReflectionCursors: serializeVisited(state.visitedReflectionCursors),
drawArtworkDataUrl: remoteArtworkDataUrl,
drawArtworkCaption: state.drawArtworkCaption,
hasDrawArtwork: Boolean(state.drawArtworkDataUrl),
}
try {
setUserDefine(ZHANGLEPING_LESSON_ID, ZHANGLEPING_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Zhangleping store', error)
}
}