import { create } from 'zustand' import { persist } from 'zustand/middleware' const DEFAULT_TUTORIAL_ID = '__default__' export interface Step { id: string title: string order: number } export interface TutorialConfig { title: string description?: string coverEmoji?: string category?: string difficulty?: string duration?: string tags?: string[] featured?: boolean steps: Step[] } interface TutorialProgress { currentStepIndex: number completedSteps: string[] } interface TutorialState { // 当前步骤索引 currentStepIndex: number // 已完成的步骤 ID completedSteps: string[] // 当前教程 ID activeTutorialId: string // 多教程进度缓存 progressByTutorial: Record // 教程配置 config: TutorialConfig | null // 侧边栏是否展开 sidebarExpanded: boolean // 切换当前教程 setActiveTutorial: (tutorialId: string, config: TutorialConfig) => void // 设置教程配置 setConfig: (config: TutorialConfig) => void // 设置当前步骤 setCurrentStep: (index: number) => void // 标记步骤完成 completeStep: (stepId: string) => void // 检查步骤是否完成 isStepCompleted: (stepId: string) => boolean // 获取完成进度 (0-100) getProgress: () => number // 获取指定教程进度 getTutorialProgress: ( tutorialId: string, totalSteps: number ) => { completedCount: number; percent: number } // 重置进度 resetProgress: () => void // 下一题 nextStep: () => void // 上一题 prevStep: () => void // 切换侧边栏 toggleSidebar: () => void } function getSafeTutorialId(tutorialId?: string) { const id = (tutorialId || '').trim() return id || DEFAULT_TUTORIAL_ID } function normalizeProgress(config: TutorialConfig, progress?: TutorialProgress): TutorialProgress { const maxIndex = Math.max(0, config.steps.length - 1) const validStepIds = new Set(config.steps.map((step) => step.id)) const currentStepIndex = Math.min(progress?.currentStepIndex ?? 0, maxIndex) const completedSteps = (progress?.completedSteps || []).filter((stepId) => validStepIds.has(stepId)) return { currentStepIndex, completedSteps, } } export const useTutorialStore = create()( persist( (set, get) => ({ currentStepIndex: 0, completedSteps: [], activeTutorialId: DEFAULT_TUTORIAL_ID, progressByTutorial: {}, config: null, sidebarExpanded: true, setActiveTutorial: (tutorialId, config) => set((state) => { const safeTutorialId = getSafeTutorialId(tutorialId) const nextProgress = normalizeProgress(config, state.progressByTutorial[safeTutorialId]) return { activeTutorialId: safeTutorialId, config, currentStepIndex: nextProgress.currentStepIndex, completedSteps: nextProgress.completedSteps, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: nextProgress, }, } }), setConfig: (config) => set((state) => { const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const baseProgress = state.progressByTutorial[safeTutorialId] || { currentStepIndex: state.currentStepIndex, completedSteps: state.completedSteps, } const nextProgress = normalizeProgress(config, baseProgress) return { config, currentStepIndex: nextProgress.currentStepIndex, completedSteps: nextProgress.completedSteps, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: nextProgress, }, } }), setCurrentStep: (index) => { const { config } = get() if (config && index >= 0 && index < config.steps.length) { set((state) => { const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const nextProgress: TutorialProgress = { currentStepIndex: index, completedSteps: state.completedSteps, } return { currentStepIndex: index, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: nextProgress, }, } }) } }, completeStep: (stepId) => { set((state) => { if (state.completedSteps.includes(stepId)) { return state } const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const nextCompletedSteps = [...state.completedSteps, stepId] return { completedSteps: nextCompletedSteps, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: { currentStepIndex: state.currentStepIndex, completedSteps: nextCompletedSteps, }, }, } }) }, isStepCompleted: (stepId) => { return get().completedSteps.includes(stepId) }, getProgress: () => { const { config, completedSteps } = get() if (!config || config.steps.length === 0) return 0 return Math.round((completedSteps.length / config.steps.length) * 100) }, getTutorialProgress: (tutorialId, totalSteps) => { if (totalSteps <= 0) { return { completedCount: 0, percent: 0 } } const safeTutorialId = getSafeTutorialId(tutorialId) const progress = get().progressByTutorial[safeTutorialId] const completedCount = progress?.completedSteps.length ?? 0 return { completedCount, percent: Math.round((completedCount / totalSteps) * 100), } }, resetProgress: () => { set((state) => { const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const nextProgress: TutorialProgress = { currentStepIndex: 0, completedSteps: [], } return { currentStepIndex: 0, completedSteps: [], progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: nextProgress, }, } }) }, nextStep: () => { const { currentStepIndex, config } = get() if (config && currentStepIndex < config.steps.length - 1) { set((state) => { const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const nextIndex = state.currentStepIndex + 1 return { currentStepIndex: nextIndex, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: { currentStepIndex: nextIndex, completedSteps: state.completedSteps, }, }, } }) } }, prevStep: () => { const { currentStepIndex } = get() if (currentStepIndex > 0) { set((state) => { const safeTutorialId = getSafeTutorialId(state.activeTutorialId) const nextIndex = state.currentStepIndex - 1 return { currentStepIndex: nextIndex, progressByTutorial: { ...state.progressByTutorial, [safeTutorialId]: { currentStepIndex: nextIndex, completedSteps: state.completedSteps, }, }, } }) } }, toggleSidebar: () => { set((state) => ({ sidebarExpanded: !state.sidebarExpanded })) }, }), { name: 'tutorial-progress', partialize: (state) => ({ activeTutorialId: state.activeTutorialId, progressByTutorial: state.progressByTutorial, currentStepIndex: state.currentStepIndex, completedSteps: state.completedSteps, }), } ) )