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

238 lines
7.2 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 ChenyunStore {
// 存储每个场景(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
// ========== 断点续读 ==========
// 上次访问的场景(place1~place9)
lastVisitedPlace: string | null
setLastVisitedPlace: (place: string) => void
// ========== 体悟模块状态 ==========
// 银元之战是否已解决
isYinyuanSolved: boolean
// 设置银元之战解决状态
setYinyuanSolved: (solved: boolean) => void
// 米棉之战是否已解决
isMimianSolved: boolean
// 设置米棉之战解决状态
setMimianSolved: (solved: boolean) => 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
}
interface ChenyunPersistedState {
visitedExhibits?: Record<string, number[]>
isYinyuanSolved?: boolean
isMimianSolved?: boolean
lastVisitedPlace?: string | null
completedVideos?: Record<string, boolean>
}
export const useChenyunStore = create<ChenyunStore>()(
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,
isYinyuanSolved: false,
isMimianSolved: false,
completedVideos: {},
})
},
// ========== 断点续读 ==========
lastVisitedPlace: null,
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
// ========== 体悟模块状态 ==========
isYinyuanSolved: false,
setYinyuanSolved: (solved: boolean) => {
set({ isYinyuanSolved: solved })
},
isMimianSolved: false,
setMimianSolved: (solved: boolean) => {
set({ isMimianSolved: solved })
},
completedVideos: {},
markVideoCompleted: (videoKey: string) => {
set((state) => ({
completedVideos: { ...state.completedVideos, [videoKey]: true },
}))
},
}),
{
name: STORE_NAMES.CHENYUN, // localStorage key
partialize: (state) => ({
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
isYinyuanSolved: state.isYinyuanSolved,
isMimianSolved: state.isMimianSolved,
lastVisitedPlace: state.lastVisitedPlace,
completedVideos: state.completedVideos,
}),
onRehydrateStorage: () => (state) => {
if (state) {
// 确保反序列化后的数据格式正确
state.visitedExhibits = deserializeVisitedExhibits(state.visitedExhibits as any)
// 确保解决状态有默认值
if (state.isYinyuanSolved === undefined) {
state.isYinyuanSolved = false
}
if (state.isMimianSolved === undefined) {
state.isMimianSolved = false
}
if (state.lastVisitedPlace === undefined) {
state.lastVisitedPlace = null
}
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
state.completedVideos = {}
}
}
},
}
)
)
// ========== 易班平台数据保存 ==========
export const CHENYUN_LESSON_ID = 'Cheny_duihua'
export const CHENYUN_KEY = 22
export const saveChenyunStore = (): void => {
const state = useChenyunStore.getState()
const payload = {
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
isYinyuanSolved: state.isYinyuanSolved,
isMimianSolved: state.isMimianSolved,
lastVisitedPlace: state.lastVisitedPlace,
completedVideos: state.completedVideos,
}
try {
setUserDefine(CHENYUN_LESSON_ID, CHENYUN_KEY, JSON.stringify(payload))
} catch (error) {
console.error('Failed to save Chenyun store', error)
}
}
export const applyChenyunPersistedState = (payload: ChenyunPersistedState): void => {
useChenyunStore.setState({
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
isYinyuanSolved: payload.isYinyuanSolved ?? false,
isMimianSolved: payload.isMimianSolved ?? false,
lastVisitedPlace: payload.lastVisitedPlace ?? null,
completedVideos: payload.completedVideos ?? {},
})
}
export const loadChenyunStore = async (): Promise<UserDefineFetchStatus> => {
try {
const result = await getUserDefineResult(CHENYUN_LESSON_ID, CHENYUN_KEY)
if (result.status !== 'success') {
if (result.status === 'empty') {
useChenyunStore.getState().resetAll()
}
return result.status
}
applyChenyunPersistedState(JSON.parse(result.content) as ChenyunPersistedState)
return result.status
} catch (error) {
console.error('Failed to load Chenyun store', error)
return 'error'
}
}