Files
shanghai-residences/src/store/sunzhongshanStore.ts
T
zhongyidianandClaude Opus 4.8 73f6df92ce chore: 更新素材和组件优化
- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居)
- 新增点击查看图标、报纸素材
- 优化视频组件、拼图组件、展品组件
- 更新陈云音频资源
- 新增输入验证文档和工具函数
- 组件布局和动画优化

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:50:40 +08:00

312 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { STORE_NAMES } from './storeNames'
import {
setUserDefine,
getUserDefineResult,
type UserDefineFetchStatus,
} from '@/lib/utils/api-common'
export interface Clue {
id: string
name: string
collected: boolean
collectedAt?: number
}
interface SunzhongshanStore {
// 存储每个场景(place)的已访问展品ID集合
// 格式:{ place1: Set<1, 2>, place2: Set<1, 2>, ... }
visitedExhibits: Record<string, Set<number>>
// 体悟模块通用布尔状态(引导是否展示、场景是否完成等)
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
// 检查展品是否已访问
isExhibitVisited: (place: string, exhibitId: number) => boolean
// 获取某个场景已访问的展品ID数组
getVisitedExhibits: (place: string) => number[]
// 重置某个场景的访问记录
resetPlace: (place: string) => void
// 重置所有访问记录
resetAll: () => void
// 设置体悟模块布尔状态
setReflectionFlag: (key: string, value: boolean) => void
// 获取体悟模块布尔状态
getReflectionFlag: (key: string) => boolean
completedVideos: Record<string, boolean>
markVideoCompleted: (videoKey: string) => 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
}
const resolveReflectionRoute = (route: unknown): string =>
typeof route === 'string' &&
['reflection', 'reflection1', 'reflection2', 'reflection3'].includes(route)
? route
: 'reflection'
interface SunzhongshanPersistedState {
visitedExhibits?: Record<string, number[]>
reflectionFlags?: Record<string, boolean>
lastVisitedPlace?: string | null
reflectionRoute?: string
clues?: Clue[]
completedVideos?: Record<string, boolean>
}
export const useSunzhongshanStore = create<SunzhongshanStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
reflectionFlags: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
clues: [],
completedVideos: {},
markVideoCompleted: (videoKey: string) => {
set((state) => ({
completedVideos: { ...state.completedVideos, [videoKey]: true },
}))
},
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: {},
reflectionFlags: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
clues: [],
completedVideos: {},
})
},
setReflectionFlag: (key: string, value: boolean) => {
set((state) => ({
reflectionFlags: {
...state.reflectionFlags,
[key]: value,
},
}))
},
getReflectionFlag: (key: string) => {
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,
completedVideos: state.completedVideos,
}),
onRehydrateStorage: () => (state) => {
if (state) {
state.visitedExhibits = deserializeVisitedExhibits(state.visitedExhibits as any)
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 = []
}
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
state.completedVideos = {}
}
}
},
}
)
)
// ========== 易班平台数据保存(对话模块) ==========
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),
reflectionFlags: state.reflectionFlags,
lastVisitedPlace: state.lastVisitedPlace,
reflectionRoute: state.reflectionRoute,
clues: state.clues,
completedVideos: state.completedVideos,
}
try {
setUserDefine(SUNZHONGSHAN_LESSON_ID, SUNZHONGSHAN_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Sunzhongshan store', error)
}
}
export const applySunzhongshanPersistedState = (payload: SunzhongshanPersistedState): void => {
useSunzhongshanStore.setState({
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
reflectionFlags: payload.reflectionFlags ?? {},
lastVisitedPlace: payload.lastVisitedPlace ?? null,
reflectionRoute: resolveReflectionRoute(payload.reflectionRoute),
clues: Array.isArray(payload.clues) ? payload.clues : [],
completedVideos: payload.completedVideos ?? {},
})
}
export const loadSunzhongshanStore = async (): Promise<UserDefineFetchStatus> => {
try {
const result = await getUserDefineResult(SUNZHONGSHAN_LESSON_ID, SUNZHONGSHAN_KEY)
if (result.status !== 'success') {
if (result.status === 'empty') {
useSunzhongshanStore.getState().resetAll()
}
return result.status
}
applySunzhongshanPersistedState(JSON.parse(result.content) as SunzhongshanPersistedState)
return result.status
} catch (error) {
console.error('Failed to load Sunzhongshan store', error)
return 'error'
}
}