Files
french-vocab/src/stores/appStore.ts
likingcode 71232cf489 feat: 法语词汇学习应用
- Vue 3 + TypeScript + Tailwind CSS
- 词汇学习和管理功能
- 支持生词本和复习
2026-03-07 05:42:32 +00:00

183 lines
4.5 KiB
TypeScript

import { create } from 'zustand';
import type { Word, StudyProgress, StudyStats, DifficultyRating } from '../types/vocabulary';
import { calculateNextReview } from '../utils/srs';
import { db, initDatabase, exportData, importData } from '../db/database';
interface AppState {
words: Word[];
progress: Map<string, StudyProgress>;
stats: StudyStats;
currentWordIndex: number;
isFlipped: boolean;
isLoading: boolean;
// Actions
init: () => Promise<void>;
setWords: (words: Word[]) => void;
addWords: (words: Word[]) => Promise<void>;
flipCard: () => void;
rateWord: (rating: DifficultyRating) => Promise<void>;
nextWord: () => void;
getCurrentWord: () => Word | null;
getDueWords: () => Word[];
exportData: () => Promise<any>;
importData: (data: any) => Promise<void>;
speak: (text: string) => void;
}
export const useAppStore = create<AppState>((set, get) => ({
words: [],
progress: new Map(),
stats: {
totalWords: 0,
masteredWords: 0,
studyingWords: 0,
streakDays: 0,
todayStudied: 0,
todayNewWords: 0,
},
currentWordIndex: 0,
isFlipped: false,
isLoading: true,
init: async () => {
await initDatabase();
// Load words from DB
const wordsFromDB = await db.words.toArray();
// Load progress from DB
const progressFromDB = await db.progress.toArray();
const progressMap = new Map<string, StudyProgress>();
progressFromDB.forEach(p => progressMap.set(p.wordId, p));
// Load stats from DB
const statsFromDB = await db.stats.get('main');
set({
words: wordsFromDB,
progress: progressMap,
stats: statsFromDB || {
totalWords: wordsFromDB.length,
masteredWords: 0,
studyingWords: 0,
streakDays: 0,
todayStudied: 0,
todayNewWords: 0,
},
isLoading: false,
});
},
setWords: (words) => set({
words,
stats: { ...get().stats, totalWords: words.length }
}),
addWords: async (newWords) => {
const wordsWithDate = newWords.map(w => ({ ...w, addedAt: new Date() }));
await db.words.bulkAdd(wordsWithDate);
const allWords = await db.words.toArray();
set({
words: allWords,
stats: { ...get().stats, totalWords: allWords.length }
});
},
flipCard: () => set((state) => ({ isFlipped: !state.isFlipped })),
rateWord: async (rating) => {
const state = get();
const currentWord = state.getCurrentWord();
if (!currentWord) return;
const existingProgress = state.progress.get(currentWord.id);
const newProgress = calculateNextReview(
existingProgress || {
wordId: currentWord.id,
interval: 0,
repetitions: 0,
easeFactor: 2.5,
nextReviewDate: new Date(),
lastStudiedDate: new Date(),
},
rating
);
// Save to IndexedDB
await db.progress.put(newProgress);
const newProgressMap = new Map(state.progress);
newProgressMap.set(currentWord.id, newProgress);
const newStats = {
...state.stats,
todayStudied: state.stats.todayStudied + 1,
};
await db.stats.put({ ...newStats, id: 'main' });
set({
progress: newProgressMap,
isFlipped: false,
stats: newStats,
});
setTimeout(() => get().nextWord(), 300);
},
nextWord: () => {
const state = get();
const dueWords = state.getDueWords();
if (dueWords.length === 0) {
set({ currentWordIndex: -1 });
return;
}
const nextIndex = state.words.findIndex(w => w.id === dueWords[0].id);
set({
currentWordIndex: nextIndex,
isFlipped: false
});
},
getCurrentWord: () => {
const state = get();
if (state.currentWordIndex < 0 || state.currentWordIndex >= state.words.length) {
return null;
}
return state.words[state.currentWordIndex];
},
getDueWords: () => {
const state = get();
const now = new Date();
return state.words.filter((word) => {
const progress = state.progress.get(word.id);
if (!progress) return true;
return new Date(progress.nextReviewDate) <= now;
});
},
exportData: async () => {
return await exportData();
},
importData: async (data) => {
await importData(data);
await get().init();
},
speak: (text: string) => {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'fr-FR';
utterance.rate = 0.8;
window.speechSynthesis.speak(utterance);
}
},
}));