feat: 法语词汇学习应用
- Vue 3 + TypeScript + Tailwind CSS - 词汇学习和管理功能 - 支持生词本和复习
This commit is contained in:
182
src/stores/appStore.ts
Normal file
182
src/stores/appStore.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user