chore: 更新素材和组件优化
- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居) - 新增点击查看图标、报纸素材 - 优化视频组件、拼图组件、展品组件 - 更新陈云音频资源 - 新增输入验证文档和工具函数 - 组件布局和动画优化 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
73d02eb62d
commit
73f6df92ce
130 files changed
+2345
-759
No files matched your search
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface ChenyunStore {
|
||||
// 存储每个场景(place)的已访问展品ID集合
|
||||
@@ -38,6 +42,10 @@ interface ChenyunStore {
|
||||
isMimianSolved: boolean
|
||||
// 设置米棉之战解决状态
|
||||
setMimianSolved: (solved: boolean) => void
|
||||
|
||||
// ========== 视频完成状态 ==========
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化
|
||||
@@ -69,6 +77,14 @@ const deserializeVisitedExhibits = (
|
||||
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) => ({
|
||||
@@ -107,7 +123,13 @@ export const useChenyunStore = create<ChenyunStore>()(
|
||||
},
|
||||
|
||||
resetAll: () => {
|
||||
set({ visitedExhibits: {} })
|
||||
set({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
isYinyuanSolved: false,
|
||||
isMimianSolved: false,
|
||||
completedVideos: {},
|
||||
})
|
||||
},
|
||||
|
||||
// ========== 断点续读 ==========
|
||||
@@ -124,6 +146,13 @@ export const useChenyunStore = create<ChenyunStore>()(
|
||||
setMimianSolved: (solved: boolean) => {
|
||||
set({ isMimianSolved: solved })
|
||||
},
|
||||
|
||||
completedVideos: {},
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORE_NAMES.CHENYUN, // localStorage key
|
||||
@@ -132,6 +161,7 @@ export const useChenyunStore = create<ChenyunStore>()(
|
||||
isYinyuanSolved: state.isYinyuanSolved,
|
||||
isMimianSolved: state.isMimianSolved,
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -147,6 +177,9 @@ export const useChenyunStore = create<ChenyunStore>()(
|
||||
if (state.lastVisitedPlace === undefined) {
|
||||
state.lastVisitedPlace = null
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -165,6 +198,7 @@ export const saveChenyunStore = (): void => {
|
||||
isYinyuanSolved: state.isYinyuanSolved,
|
||||
isMimianSolved: state.isMimianSolved,
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -173,3 +207,31 @@ export const saveChenyunStore = (): void => {
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine, getUserDefineResult } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface HuangyanpeiStore {
|
||||
// 存储每个场景(place)的已访问展品ID集合
|
||||
@@ -59,6 +63,10 @@ interface HuangyanpeiStore {
|
||||
|
||||
// 重置所有reflection cursor记录
|
||||
resetAllReflectionCursors: () => void
|
||||
|
||||
// ========== 视频完成状态 ==========
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化
|
||||
@@ -90,6 +98,22 @@ const deserializeVisitedExhibits = (
|
||||
return visitedExhibits
|
||||
}
|
||||
|
||||
const resolveReflectionRoute = (route: unknown): string =>
|
||||
typeof route === 'string' &&
|
||||
['reflection', 'reflection2', 'reflection3', 'reflection4'].includes(route)
|
||||
? route
|
||||
: 'reflection'
|
||||
|
||||
interface HuangyanpeiPersistedState {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
visitedReflectionCursors?: Record<string, number[]>
|
||||
shownReflectionCompletionGuides?: Record<string, boolean>
|
||||
reflectionScene2Choice?: '铁工科' | '商科' | null
|
||||
lastVisitedPlace?: string | null
|
||||
reflectionRoute?: string
|
||||
completedVideos?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -132,7 +156,15 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
|
||||
},
|
||||
|
||||
resetAll: () => {
|
||||
set({ visitedExhibits: {} })
|
||||
set({
|
||||
visitedExhibits: {},
|
||||
visitedReflectionCursors: {},
|
||||
shownReflectionCompletionGuides: {},
|
||||
reflectionScene2Choice: null,
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
completedVideos: {},
|
||||
})
|
||||
},
|
||||
|
||||
markReflectionCompletionGuideShown: (scene: string) => {
|
||||
@@ -191,6 +223,13 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
|
||||
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
|
||||
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
|
||||
setReflectionScene2Choice: (choice) => set({ reflectionScene2Choice: choice }),
|
||||
|
||||
completedVideos: {},
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORE_NAMES.HUANGYANPEI, // localStorage key
|
||||
@@ -201,6 +240,7 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
|
||||
reflectionScene2Choice: state.reflectionScene2Choice,
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -221,6 +261,9 @@ export const useHuangyanpeiStore = create<HuangyanpeiStore>()(
|
||||
if ((state as any).reflectionRoute === undefined) {
|
||||
;(state as any).reflectionRoute = 'reflection'
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -236,6 +279,7 @@ export const saveHuangyanpeiStore = (): void => {
|
||||
|
||||
const payload = {
|
||||
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
|
||||
visitedReflectionCursors: serializeVisitedExhibits(state.visitedReflectionCursors),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
shownReflectionCompletionGuides: state.shownReflectionCompletionGuides,
|
||||
@@ -249,43 +293,32 @@ export const saveHuangyanpeiStore = (): void => {
|
||||
}
|
||||
}
|
||||
|
||||
export const loadHuangyanpeiStore = async (): Promise<void> => {
|
||||
export const applyHuangyanpeiPersistedState = (payload: HuangyanpeiPersistedState): void => {
|
||||
useHuangyanpeiStore.setState({
|
||||
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
|
||||
visitedReflectionCursors: deserializeVisitedExhibits(payload.visitedReflectionCursors),
|
||||
shownReflectionCompletionGuides: payload.shownReflectionCompletionGuides ?? {},
|
||||
reflectionScene2Choice: payload.reflectionScene2Choice ?? null,
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
reflectionRoute: resolveReflectionRoute(payload.reflectionRoute),
|
||||
completedVideos: payload.completedVideos ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
export const loadHuangyanpeiStore = async (): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
|
||||
if (result.status !== 'success') {
|
||||
useHuangyanpeiStore.setState({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
shownReflectionCompletionGuides: {},
|
||||
reflectionScene2Choice: null,
|
||||
})
|
||||
return
|
||||
if (result.status === 'empty') {
|
||||
useHuangyanpeiStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
const payload = JSON.parse(result.content) as {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
lastVisitedPlace?: string | null
|
||||
reflectionRoute?: string
|
||||
shownReflectionCompletionGuides?: Record<string, boolean>
|
||||
reflectionScene2Choice?: '铁工科' | '商科' | null
|
||||
}
|
||||
|
||||
useHuangyanpeiStore.setState({
|
||||
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
reflectionRoute: payload.reflectionRoute ?? 'reflection',
|
||||
shownReflectionCompletionGuides: payload.shownReflectionCompletionGuides ?? {},
|
||||
reflectionScene2Choice: payload.reflectionScene2Choice ?? null,
|
||||
})
|
||||
applyHuangyanpeiPersistedState(JSON.parse(result.content) as HuangyanpeiPersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
useHuangyanpeiStore.setState({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
shownReflectionCompletionGuides: {},
|
||||
reflectionScene2Choice: null,
|
||||
})
|
||||
console.error('Failed to load Huangyanpei store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
+65
-2
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface LuxunStore {
|
||||
// 存储每个场景(place)的已访问展品ID集合
|
||||
@@ -30,6 +34,9 @@ interface LuxunStore {
|
||||
|
||||
// 重置所有访问记录
|
||||
resetAll: () => void
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化
|
||||
@@ -61,12 +68,31 @@ const deserializeVisitedExhibits = (
|
||||
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) => {
|
||||
@@ -101,7 +127,12 @@ export const useLuxunStore = create<LuxunStore>()(
|
||||
},
|
||||
|
||||
resetAll: () => {
|
||||
set({ visitedExhibits: {} })
|
||||
set({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
completedVideos: {},
|
||||
})
|
||||
},
|
||||
|
||||
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
|
||||
@@ -113,6 +144,7 @@ export const useLuxunStore = create<LuxunStore>()(
|
||||
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -123,6 +155,9 @@ export const useLuxunStore = create<LuxunStore>()(
|
||||
if ((state as any).reflectionRoute === undefined) {
|
||||
;(state as any).reflectionRoute = 'reflection'
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -140,6 +175,7 @@ export const saveLuxunStore = (): void => {
|
||||
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -148,3 +184,30 @@ export const saveLuxunStore = (): void => {
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface MaozedongStore {
|
||||
// 每个场景(placeX)的已访问展品ID集合
|
||||
@@ -21,6 +25,9 @@ interface MaozedongStore {
|
||||
resetPlace: (place: string) => void
|
||||
// 重置所有
|
||||
resetAll: () => void
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
const serializeVisited = (data: Record<string, Set<number>>): Record<string, number[]> => {
|
||||
@@ -38,14 +45,27 @@ const deserializeVisited = (
|
||||
return out
|
||||
}
|
||||
|
||||
interface MaozedongPersistedState {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
lastVisitedPlace?: string | null
|
||||
completedVideos?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const useMaozedongStore = create<MaozedongStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
completedVideos: {},
|
||||
|
||||
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
|
||||
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
|
||||
markExhibitVisited: (place: string, exhibitId: number) => {
|
||||
set((state) => {
|
||||
const current = state.visitedExhibits[place] || new Set<number>()
|
||||
@@ -77,18 +97,21 @@ export const useMaozedongStore = create<MaozedongStore>()(
|
||||
})
|
||||
},
|
||||
|
||||
resetAll: () => set({ visitedExhibits: {}, lastVisitedPlace: null }),
|
||||
resetAll: () => set({ visitedExhibits: {}, lastVisitedPlace: null, completedVideos: {} }),
|
||||
}),
|
||||
{
|
||||
name: STORE_NAMES.MAOZEDONG,
|
||||
partialize: (state) => ({
|
||||
visitedExhibits: serializeVisited(state.visitedExhibits),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
state.visitedExhibits = deserializeVisited(state.visitedExhibits as any)
|
||||
if (state.lastVisitedPlace === undefined) state.lastVisitedPlace = null
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object')
|
||||
state.completedVideos = {}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -104,6 +127,7 @@ export const saveMaozedongStore = (): void => {
|
||||
const payload = {
|
||||
visitedExhibits: serializeVisited(state.visitedExhibits),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
try {
|
||||
setUserDefine(MAOZEDONG_LESSON_ID, MAOZEDONG_KEY, JSON.stringify(payload))
|
||||
@@ -111,3 +135,29 @@ export const saveMaozedongStore = (): void => {
|
||||
console.error('Failed to save Maozedong store', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const applyMaozedongPersistedState = (payload: MaozedongPersistedState): void => {
|
||||
useMaozedongStore.setState({
|
||||
visitedExhibits: deserializeVisited(payload.visitedExhibits),
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
completedVideos: payload.completedVideos ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
export const loadMaozedongStore = async (): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(MAOZEDONG_LESSON_ID, MAOZEDONG_KEY)
|
||||
if (result.status !== 'success') {
|
||||
if (result.status === 'empty') {
|
||||
useMaozedongStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
applyMaozedongPersistedState(JSON.parse(result.content) as MaozedongPersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
console.error('Failed to load Maozedong store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
+73
-2
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface NieerStore {
|
||||
// 存储每个场景(place)的已访问展品ID集合
|
||||
@@ -59,6 +63,9 @@ interface NieerStore {
|
||||
// 是否已经看过旅行箱提示
|
||||
hasSeenBoxHint: boolean
|
||||
markBoxHintSeen: () => void
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化
|
||||
@@ -90,10 +97,27 @@ const deserializeVisitedExhibits = (
|
||||
return visitedExhibits
|
||||
}
|
||||
|
||||
interface NieerPersistedState {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
visitedReflectionCursors?: Record<string, number[]>
|
||||
lastVisitedPlace?: string | null
|
||||
reflectionWorks?: Record<number, Record<string, unknown>>
|
||||
hasSeenReflectionGuide?: boolean
|
||||
hasSeenBoxHint?: boolean
|
||||
completedVideos?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const useNieerStore = create<NieerStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
visitedExhibits: {},
|
||||
completedVideos: {},
|
||||
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
|
||||
markExhibitVisited: (place: string, exhibitId: number) => {
|
||||
set((state) => {
|
||||
@@ -128,7 +152,15 @@ export const useNieerStore = create<NieerStore>()(
|
||||
},
|
||||
|
||||
resetAll: () => {
|
||||
set({ visitedExhibits: {} })
|
||||
set({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
visitedReflectionCursors: {},
|
||||
reflectionWorks: {},
|
||||
hasSeenReflectionGuide: false,
|
||||
hasSeenBoxHint: false,
|
||||
completedVideos: {},
|
||||
})
|
||||
},
|
||||
|
||||
lastVisitedPlace: null,
|
||||
@@ -210,6 +242,7 @@ export const useNieerStore = create<NieerStore>()(
|
||||
reflectionWorks: state.reflectionWorks,
|
||||
hasSeenReflectionGuide: state.hasSeenReflectionGuide,
|
||||
hasSeenBoxHint: state.hasSeenBoxHint,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -229,6 +262,9 @@ export const useNieerStore = create<NieerStore>()(
|
||||
if ((state as any).hasSeenBoxHint === undefined) {
|
||||
;(state as any).hasSeenBoxHint = false
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -243,7 +279,12 @@ export const saveNieerStore = (): void => {
|
||||
const state = useNieerStore.getState()
|
||||
const payload = {
|
||||
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
|
||||
visitedReflectionCursors: serializeVisitedExhibits(state.visitedReflectionCursors),
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionWorks: state.reflectionWorks,
|
||||
hasSeenReflectionGuide: state.hasSeenReflectionGuide,
|
||||
hasSeenBoxHint: state.hasSeenBoxHint,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
try {
|
||||
setUserDefine(NIEER_LESSON_ID, NIEER_KEY, JSON.stringify(payload))
|
||||
@@ -251,3 +292,33 @@ export const saveNieerStore = (): void => {
|
||||
console.error('Failed to save Nieer store', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const applyNieerPersistedState = (payload: NieerPersistedState): void => {
|
||||
useNieerStore.setState({
|
||||
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
|
||||
visitedReflectionCursors: deserializeVisitedExhibits(payload.visitedReflectionCursors),
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
reflectionWorks: payload.reflectionWorks ?? {},
|
||||
hasSeenReflectionGuide: payload.hasSeenReflectionGuide ?? false,
|
||||
hasSeenBoxHint: payload.hasSeenBoxHint ?? false,
|
||||
completedVideos: payload.completedVideos ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
export const loadNieerStore = async (): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(NIEER_LESSON_ID, NIEER_KEY)
|
||||
if (result.status !== 'success') {
|
||||
if (result.status === 'empty') {
|
||||
useNieerStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
applyNieerPersistedState(JSON.parse(result.content) as NieerPersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
console.error('Failed to load Nieer store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type StorageOptions,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface PuzzleStore {
|
||||
// 每个故居的已解锁碎片集合,格式:{ residence: Set<pieceId> }
|
||||
@@ -25,6 +30,9 @@ interface PuzzleStore {
|
||||
// 重置所有碎片
|
||||
resetAll: () => void
|
||||
|
||||
// 应用持久化的拼图数据
|
||||
applyPersistedState: (payload: PuzzlePersistedState) => void
|
||||
|
||||
// 获取收集进度
|
||||
getPuzzleCollectProgress: (residence: string) => number
|
||||
}
|
||||
@@ -56,6 +64,10 @@ export const deserializePuzzles = (serialized: Record<string, number[]> | undefi
|
||||
return puzzles
|
||||
}
|
||||
|
||||
interface PuzzlePersistedState {
|
||||
puzzles?: Record<string, number[]>
|
||||
}
|
||||
|
||||
// 辅助函数:确保 puzzles 中的值都是 Set
|
||||
const ensurePuzzlesAreSets = (puzzles: any): Record<string, Set<number>> => {
|
||||
const fixed: Record<string, Set<number>> = {}
|
||||
@@ -140,6 +152,10 @@ export const usePuzzleStore = create<PuzzleStore>()(
|
||||
resetAll: () => {
|
||||
set({ puzzles: {} })
|
||||
},
|
||||
|
||||
applyPersistedState: (payload: PuzzlePersistedState) => {
|
||||
set({ puzzles: deserializePuzzles(payload.puzzles) })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORE_NAMES.PUZZLE, // localStorage key
|
||||
@@ -180,3 +196,25 @@ export const savePuzzleStore = (puzzles: Record<string, Set<number>>): void => {
|
||||
console.error('Failed to save Chenyun store', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadPuzzleStore = async (
|
||||
options: StorageOptions = {}
|
||||
): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(PUZZLE_LESSON_ID, PUZZLE_KEY, options)
|
||||
if (result.status !== 'success') {
|
||||
if (result.status === 'empty') {
|
||||
usePuzzleStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
usePuzzleStore
|
||||
.getState()
|
||||
.applyPersistedState(JSON.parse(result.content) as PuzzlePersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
console.error('Failed to load puzzle store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface RongdeshengStore {
|
||||
// 对话模块:每个 place 的已访问展品 ID 集合
|
||||
@@ -45,6 +49,9 @@ interface RongdeshengStore {
|
||||
|
||||
// 重置所有访问记录
|
||||
resetAll: () => void
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
const serializeVisitedExhibits = (
|
||||
@@ -68,6 +75,16 @@ const deserializeVisitedExhibits = (
|
||||
return visitedExhibits
|
||||
}
|
||||
|
||||
interface RongdeshengPersistedState {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
lastVisitedPlace?: string | null
|
||||
reflectionCompleted?: Record<string, boolean>
|
||||
reflectionVideoViewed?: Record<string, Record<string, boolean>>
|
||||
reflectionFinalCompleted?: boolean
|
||||
reflectionGuideShown?: boolean
|
||||
completedVideos?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const useRongdeshengStore = create<RongdeshengStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -77,9 +94,16 @@ export const useRongdeshengStore = create<RongdeshengStore>()(
|
||||
reflectionVideoViewed: {},
|
||||
reflectionFinalCompleted: false,
|
||||
reflectionGuideShown: false,
|
||||
completedVideos: {},
|
||||
|
||||
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
|
||||
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
|
||||
markReflectionVideoViewed: (sceneId: string, videoId: string) => {
|
||||
set((state) => ({
|
||||
reflectionVideoViewed: {
|
||||
@@ -155,6 +179,7 @@ export const useRongdeshengStore = create<RongdeshengStore>()(
|
||||
reflectionVideoViewed: {},
|
||||
reflectionFinalCompleted: false,
|
||||
reflectionGuideShown: false,
|
||||
completedVideos: {},
|
||||
}),
|
||||
}),
|
||||
{
|
||||
@@ -166,6 +191,7 @@ export const useRongdeshengStore = create<RongdeshengStore>()(
|
||||
reflectionVideoViewed: state.reflectionVideoViewed,
|
||||
reflectionFinalCompleted: state.reflectionFinalCompleted,
|
||||
reflectionGuideShown: state.reflectionGuideShown,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return
|
||||
@@ -183,6 +209,9 @@ export const useRongdeshengStore = create<RongdeshengStore>()(
|
||||
if (typeof state.reflectionGuideShown !== 'boolean') {
|
||||
state.reflectionGuideShown = false
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -201,6 +230,7 @@ export const saveRongdeshengStore = (): void => {
|
||||
reflectionVideoViewed: state.reflectionVideoViewed,
|
||||
reflectionFinalCompleted: state.reflectionFinalCompleted,
|
||||
reflectionGuideShown: state.reflectionGuideShown,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
try {
|
||||
setUserDefine(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY, JSON.stringify(payload))
|
||||
@@ -208,3 +238,33 @@ export const saveRongdeshengStore = (): void => {
|
||||
console.error('Failed to save Rongdesheng store', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const applyRongdeshengPersistedState = (payload: RongdeshengPersistedState): void => {
|
||||
useRongdeshengStore.setState({
|
||||
visitedExhibits: deserializeVisitedExhibits(payload.visitedExhibits),
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
reflectionCompleted: payload.reflectionCompleted ?? {},
|
||||
reflectionVideoViewed: payload.reflectionVideoViewed ?? {},
|
||||
reflectionFinalCompleted: payload.reflectionFinalCompleted ?? false,
|
||||
reflectionGuideShown: payload.reflectionGuideShown ?? false,
|
||||
completedVideos: payload.completedVideos ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
export const loadRongdeshengStore = async (): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY)
|
||||
if (result.status !== 'success') {
|
||||
if (result.status === 'empty') {
|
||||
useRongdeshengStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
applyRongdeshengPersistedState(JSON.parse(result.content) as RongdeshengPersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
console.error('Failed to load Rongdesheng store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
export interface Clue {
|
||||
id: string
|
||||
@@ -53,6 +57,9 @@ interface SunzhongshanStore {
|
||||
|
||||
// 获取体悟模块布尔状态
|
||||
getReflectionFlag: (key: string) => boolean
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化
|
||||
@@ -84,6 +91,21 @@ const deserializeVisitedExhibits = (
|
||||
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) => ({
|
||||
@@ -92,6 +114,13 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
clues: [],
|
||||
completedVideos: {},
|
||||
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
|
||||
markExhibitVisited: (place: string, exhibitId: number) => {
|
||||
set((state) => {
|
||||
@@ -132,6 +161,7 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
clues: [],
|
||||
completedVideos: {},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -202,6 +232,7 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
clues: state.clues,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -218,6 +249,9 @@ export const useSunzhongshanStore = create<SunzhongshanStore>()(
|
||||
if (!(state as any).clues || !Array.isArray((state as any).clues)) {
|
||||
;(state as any).clues = []
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -233,8 +267,11 @@ export const saveSunzhongshanStore = (): void => {
|
||||
|
||||
const payload = {
|
||||
visitedExhibits: serializeVisitedExhibits(state.visitedExhibits),
|
||||
reflectionFlags: state.reflectionFlags,
|
||||
lastVisitedPlace: state.lastVisitedPlace,
|
||||
reflectionRoute: state.reflectionRoute,
|
||||
clues: state.clues,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -243,3 +280,32 @@ export const saveSunzhongshanStore = (): void => {
|
||||
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'
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -2,11 +2,15 @@ import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
|
||||
export type YibanConnectionStatus = 'unknown' | 'connected' | 'disconnected'
|
||||
|
||||
interface UserStore {
|
||||
courseId: string | null
|
||||
uid: string | null
|
||||
yibanConnectionStatus: YibanConnectionStatus
|
||||
|
||||
setCourseInfo: (info: { courseId?: string | null; uid?: string | null }) => void
|
||||
setYibanConnectionStatus: (status: YibanConnectionStatus) => void
|
||||
resetCourseInfo: () => void
|
||||
}
|
||||
|
||||
@@ -15,6 +19,7 @@ export const useUserStore = create<UserStore>()(
|
||||
(set) => ({
|
||||
courseId: null,
|
||||
uid: null,
|
||||
yibanConnectionStatus: 'unknown',
|
||||
|
||||
setCourseInfo: (info) => {
|
||||
set((state) => ({
|
||||
@@ -23,12 +28,20 @@ export const useUserStore = create<UserStore>()(
|
||||
}))
|
||||
},
|
||||
|
||||
setYibanConnectionStatus: (status) => {
|
||||
set({ yibanConnectionStatus: status })
|
||||
},
|
||||
|
||||
resetCourseInfo: () => {
|
||||
set({ courseId: null, uid: null })
|
||||
set({ courseId: null, uid: null, yibanConnectionStatus: 'unknown' })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORE_NAMES.USER,
|
||||
partialize: (state) => ({
|
||||
courseId: state.courseId,
|
||||
uid: state.uid,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { STORE_NAMES } from './storeNames'
|
||||
import { setUserDefine } from '@/lib/utils/api-common'
|
||||
import {
|
||||
setUserDefine,
|
||||
getUserDefineResult,
|
||||
type UserDefineFetchStatus,
|
||||
} from '@/lib/utils/api-common'
|
||||
|
||||
interface ZhanglepingStore {
|
||||
// ========= 对话模块:展品访问进度/断点 =========
|
||||
@@ -13,6 +17,7 @@ interface ZhanglepingStore {
|
||||
getVisitedExhibits: (place: string) => number[]
|
||||
resetPlace: (place: string) => void
|
||||
resetAllDialog: () => void
|
||||
resetAll: () => void
|
||||
|
||||
// 体悟模块当前路由(reflection / reflection1 / ... / reflection5)
|
||||
reflectionRoute: string
|
||||
@@ -38,6 +43,9 @@ interface ZhanglepingStore {
|
||||
// 是否已经看过学习引导窗口
|
||||
hasSeenStudyGuide: boolean
|
||||
markStudyGuideSeen: () => void
|
||||
|
||||
completedVideos: Record<string, boolean>
|
||||
markVideoCompleted: (videoKey: string) => void
|
||||
}
|
||||
|
||||
// 将 Set 转换为数组以便序列化(体悟)
|
||||
@@ -61,6 +69,31 @@ const deserializeVisited = (
|
||||
return visited
|
||||
}
|
||||
|
||||
const resolveReflectionRoute = (route: unknown): string =>
|
||||
typeof route === 'string' &&
|
||||
[
|
||||
'reflection',
|
||||
'reflection1',
|
||||
'reflection2',
|
||||
'reflection3',
|
||||
'reflection4',
|
||||
'reflection5',
|
||||
].includes(route)
|
||||
? route
|
||||
: 'reflection'
|
||||
|
||||
interface ZhanglepingPersistedState {
|
||||
visitedExhibits?: Record<string, number[]>
|
||||
lastVisitedPlace?: string | null
|
||||
reflectionRoute?: string
|
||||
visitedReflectionCursors?: Record<string, number[]>
|
||||
drawArtworkDataUrl?: string
|
||||
drawArtworkCaption?: string
|
||||
hasDrawArtwork?: boolean
|
||||
hasSeenStudyGuide?: boolean
|
||||
completedVideos?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const useZhanglepingStore = create<ZhanglepingStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -72,6 +105,13 @@ export const useZhanglepingStore = create<ZhanglepingStore>()(
|
||||
drawArtworkCaption: '',
|
||||
hasDrawArtwork: false,
|
||||
hasSeenStudyGuide: false,
|
||||
completedVideos: {},
|
||||
|
||||
markVideoCompleted: (videoKey: string) => {
|
||||
set((state) => ({
|
||||
completedVideos: { ...state.completedVideos, [videoKey]: true },
|
||||
}))
|
||||
},
|
||||
|
||||
markExhibitVisited: (place: string, exhibitId: number) => {
|
||||
set((state) => {
|
||||
@@ -101,6 +141,18 @@ export const useZhanglepingStore = create<ZhanglepingStore>()(
|
||||
})
|
||||
},
|
||||
resetAllDialog: () => set({ visitedExhibits: {}, lastVisitedPlace: null }),
|
||||
resetAll: () =>
|
||||
set({
|
||||
visitedExhibits: {},
|
||||
lastVisitedPlace: null,
|
||||
reflectionRoute: 'reflection',
|
||||
visitedReflectionCursors: {},
|
||||
drawArtworkDataUrl: '',
|
||||
drawArtworkCaption: '',
|
||||
hasDrawArtwork: false,
|
||||
hasSeenStudyGuide: false,
|
||||
completedVideos: {},
|
||||
}),
|
||||
setLastVisitedPlace: (place: string) => set({ lastVisitedPlace: place }),
|
||||
|
||||
setReflectionRoute: (route: string) => set({ reflectionRoute: route }),
|
||||
@@ -150,6 +202,7 @@ export const useZhanglepingStore = create<ZhanglepingStore>()(
|
||||
drawArtworkCaption: state.drawArtworkCaption,
|
||||
hasDrawArtwork: state.hasDrawArtwork,
|
||||
hasSeenStudyGuide: state.hasSeenStudyGuide,
|
||||
completedVideos: state.completedVideos,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return
|
||||
@@ -173,6 +226,9 @@ export const useZhanglepingStore = create<ZhanglepingStore>()(
|
||||
if ((state as any).hasSeenStudyGuide === undefined) {
|
||||
;(state as any).hasSeenStudyGuide = false
|
||||
}
|
||||
if (!state.completedVideos || typeof state.completedVideos !== 'object') {
|
||||
state.completedVideos = {}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -199,6 +255,8 @@ export const saveZhanglepingStore = (): void => {
|
||||
drawArtworkDataUrl: remoteArtworkDataUrl,
|
||||
drawArtworkCaption: state.drawArtworkCaption,
|
||||
hasDrawArtwork: state.hasDrawArtwork,
|
||||
hasSeenStudyGuide: state.hasSeenStudyGuide,
|
||||
completedVideos: state.completedVideos,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -207,3 +265,35 @@ export const saveZhanglepingStore = (): void => {
|
||||
console.error('Failed to save Zhangleping store', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const applyZhanglepingPersistedState = (payload: ZhanglepingPersistedState): void => {
|
||||
useZhanglepingStore.setState({
|
||||
visitedExhibits: deserializeVisited(payload.visitedExhibits),
|
||||
lastVisitedPlace: payload.lastVisitedPlace ?? null,
|
||||
reflectionRoute: resolveReflectionRoute(payload.reflectionRoute),
|
||||
visitedReflectionCursors: deserializeVisited(payload.visitedReflectionCursors),
|
||||
drawArtworkDataUrl: payload.drawArtworkDataUrl ?? '',
|
||||
drawArtworkCaption: payload.drawArtworkCaption ?? '',
|
||||
hasDrawArtwork: payload.hasDrawArtwork ?? false,
|
||||
hasSeenStudyGuide: payload.hasSeenStudyGuide ?? false,
|
||||
completedVideos: payload.completedVideos ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
export const loadZhanglepingStore = async (): Promise<UserDefineFetchStatus> => {
|
||||
try {
|
||||
const result = await getUserDefineResult(ZHANGLEPING_LESSON_ID, ZHANGLEPING_KEY)
|
||||
if (result.status !== 'success') {
|
||||
if (result.status === 'empty') {
|
||||
useZhanglepingStore.getState().resetAll()
|
||||
}
|
||||
return result.status
|
||||
}
|
||||
|
||||
applyZhanglepingPersistedState(JSON.parse(result.content) as ZhanglepingPersistedState)
|
||||
return result.status
|
||||
} catch (error) {
|
||||
console.error('Failed to load Zhangleping store', error)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user