chore: 更新素材和组件优化

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.8 committed 2026-06-21 21:50:40 +08:00
1 parent 73d02eb62d
commit 73f6df92ce
130 files changed
+2345 -759

No files matched your search

+64 -2
View File
@@ -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'
}
}
+66 -33
View File
@@ -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
View File
@@ -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'
}
}
+52 -2
View File
@@ -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
View File
@@ -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'
}
}
+39 -1
View File
@@ -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'
}
}
+61 -1
View File
@@ -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'
}
}
+67 -1
View File
@@ -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
View File
@@ -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,
}),
}
)
)
+91 -1
View File
@@ -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'
}
}