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> // 体悟模块通用布尔状态(引导是否展示、场景是否完成等) reflectionFlags: Record // 上次访问的场景(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 markVideoCompleted: (videoKey: string) => void } // 将 Set 转换为数组以便序列化 const serializeVisitedExhibits = ( visitedExhibits: Record> ): Record => { const serialized: Record = {} for (const [place, exhibits] of Object.entries(visitedExhibits)) { serialized[place] = Array.from(exhibits) } return serialized } // 将数组转换回 Set const deserializeVisitedExhibits = ( serialized: Record | undefined ): Record> => { if (!serialized || typeof serialized !== 'object') { return {} } const visitedExhibits: Record> = {} 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 reflectionFlags?: Record lastVisitedPlace?: string | null reflectionRoute?: string clues?: Clue[] completedVideos?: Record } export const useSunzhongshanStore = create()( 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() 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 => { 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' } }