Files
shanghai-residences/src/store/luxunStore.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

214 lines
6.5 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'
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
// 检查展品是否已访问
isExhibitVisited: (place: string, exhibitId: number) => boolean
// 获取某个场景已访问的展品ID数组
getVisitedExhibits: (place: string) => number[]
// 重置某个场景的访问记录
resetPlace: (place: string) => void
// 重置所有访问记录
resetAll: () => void
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', 'reflection2', 'reflection3'].includes(route)
? route
: 'reflection'
interface LuxunPersistedState {
visitedExhibits?: Record<string, number[]>
lastVisitedPlace?: string | null
reflectionRoute?: string
completedVideos?: Record<string, boolean>
}
export const useLuxunStore = create<LuxunStore>()(
persist(
(set, get) => ({
visitedExhibits: {},
reflectionRoute: 'reflection',
lastVisitedPlace: null,
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: {},
lastVisitedPlace: null,
reflectionRoute: 'reflection',
completedVideos: {},
})
},
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,
completedVideos: state.completedVideos,
}),
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'
}
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
state.completedVideos = {}
}
}
},
}
)
)
// ========== 易班平台数据保存(对话模块) ==========
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,
completedVideos: state.completedVideos,
}
try {
setUserDefine(LUXUN_LESSON_ID, LUXUN_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Luxun store', error)
}
}
export const applyLuxunPersistedState = (payload: LuxunPersistedState): void => {
useLuxunStore.setState({
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
lastVisitedPlace: payload.lastVisitedPlace ?? null,
reflectionRoute: resolveReflectionRoute(payload.reflectionRoute),
completedVideos: payload.completedVideos ?? {},
})
}
export const loadLuxunStore = async (): Promise<UserDefineFetchStatus> => {
try {
const result = await getUserDefineResult(LUXUN_LESSON_ID, LUXUN_KEY)
if (result.status !== 'success') {
if (result.status === 'empty') {
useLuxunStore.getState().resetAll()
}
return result.status
}
applyLuxunPersistedState(JSON.parse(result.content) as LuxunPersistedState)
return result.status
} catch (error) {
console.error('Failed to load Luxun store', error)
return 'error'
}
}