349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { ref, computed } from 'vue'
|
||
import type {
|
||
VocabularyReviewData,
|
||
ReviewSession,
|
||
ReviewResponse,
|
||
WeaknessPattern
|
||
} from '@/utils/spacedRepetition'
|
||
import {
|
||
SpacedRepetitionAlgorithm,
|
||
createDefaultVocabularyReviewData
|
||
} from '@/utils/spacedRepetition'
|
||
|
||
export interface LearningPlan {
|
||
date: string
|
||
vocabulary: VocabularyReviewData[]
|
||
totalCount: number
|
||
estimatedTime: number // 分鐘
|
||
}
|
||
|
||
export interface ReviewStats {
|
||
todayCompleted: number
|
||
todayTotal: number
|
||
weeklyStreak: number
|
||
totalMastered: number
|
||
averageAccuracy: number
|
||
improvementTrend: number
|
||
nextReviewTime: Date | null
|
||
}
|
||
|
||
export const useReviewStore = defineStore('review', () => {
|
||
// 狀態
|
||
const vocabularyReviewData = ref<Map<string, VocabularyReviewData>>(new Map())
|
||
const reviewHistory = ref<ReviewSession[]>([])
|
||
const currentReviewSession = ref<ReviewSession | null>(null)
|
||
const learningPlan = ref<Map<string, LearningPlan>>(new Map())
|
||
const isLoading = ref(false)
|
||
const algorithm = new SpacedRepetitionAlgorithm()
|
||
|
||
// 計算屬性
|
||
const todaysReviewVocabulary = computed(() => {
|
||
const allVocabulary = Array.from(vocabularyReviewData.value.values())
|
||
return SpacedRepetitionAlgorithm.getTodaysReviewVocabulary(allVocabulary)
|
||
})
|
||
|
||
const reviewStats = computed((): ReviewStats => {
|
||
const todayTotal = todaysReviewVocabulary.value.length
|
||
const todayCompleted = reviewHistory.value.filter(session => {
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
const sessionDate = new Date(session.startTime)
|
||
sessionDate.setHours(0, 0, 0, 0)
|
||
return sessionDate.getTime() === today.getTime()
|
||
}).length
|
||
|
||
const allVocabulary = Array.from(vocabularyReviewData.value.values())
|
||
const totalMastered = allVocabulary.filter(v => v.masteryLevel >= 80).length
|
||
|
||
const efficiency = SpacedRepetitionAlgorithm.analyzeLearningEfficiency(reviewHistory.value)
|
||
|
||
// 計算連續學習天數
|
||
const weeklyStreak = calculateWeeklyStreak()
|
||
|
||
// 下次複習時間
|
||
const nextReviewTime = getNextReviewTime()
|
||
|
||
return {
|
||
todayCompleted,
|
||
todayTotal,
|
||
weeklyStreak,
|
||
totalMastered,
|
||
averageAccuracy: efficiency.averageAccuracy,
|
||
improvementTrend: efficiency.improvementTrend,
|
||
nextReviewTime
|
||
}
|
||
})
|
||
|
||
const urgentReviewVocabulary = computed(() => {
|
||
const today = new Date()
|
||
return todaysReviewVocabulary.value.filter(vocab => {
|
||
const overdueDays = Math.floor((today.getTime() - vocab.nextReviewDate.getTime()) / (24 * 60 * 60 * 1000))
|
||
return overdueDays > 2 // 過期超過2天視為緊急
|
||
})
|
||
})
|
||
|
||
const weaknessAnalysis = computed(() => {
|
||
const allPatterns = new Map<string, { severity: number, frequency: number }>()
|
||
|
||
Array.from(vocabularyReviewData.value.values()).forEach(vocab => {
|
||
vocab.weaknessPatterns.forEach(pattern => {
|
||
const existing = allPatterns.get(pattern.type) || { severity: 0, frequency: 0 }
|
||
allPatterns.set(pattern.type, {
|
||
severity: Math.max(existing.severity, pattern.severity),
|
||
frequency: existing.frequency + pattern.frequency
|
||
})
|
||
})
|
||
})
|
||
|
||
return Array.from(allPatterns.entries())
|
||
.map(([type, data]) => ({
|
||
type,
|
||
severity: data.severity,
|
||
frequency: data.frequency,
|
||
score: data.severity * Math.log(data.frequency + 1)
|
||
}))
|
||
.sort((a, b) => b.score - a.score)
|
||
.slice(0, 5) // 前5個最嚴重的薄弱點
|
||
})
|
||
|
||
// 方法
|
||
const initializeVocabularyReviewData = (vocabularyIds: string[]) => {
|
||
vocabularyIds.forEach(id => {
|
||
if (!vocabularyReviewData.value.has(id)) {
|
||
vocabularyReviewData.value.set(id, createDefaultVocabularyReviewData(id))
|
||
}
|
||
})
|
||
}
|
||
|
||
const startReviewSession = (vocabularyIds: string[]): string => {
|
||
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||
|
||
currentReviewSession.value = {
|
||
vocabularyId: vocabularyIds[0], // 如果是批量複習,這裡需要調整
|
||
startTime: new Date(),
|
||
responses: [],
|
||
overallAccuracy: 0,
|
||
averageResponseTime: 0
|
||
}
|
||
|
||
return sessionId
|
||
}
|
||
|
||
const addReviewResponse = (response: Omit<ReviewResponse, 'timestamp'>) => {
|
||
if (!currentReviewSession.value) {
|
||
throw new Error('沒有活躍的複習會話')
|
||
}
|
||
|
||
const fullResponse: ReviewResponse = {
|
||
...response,
|
||
timestamp: new Date()
|
||
}
|
||
|
||
currentReviewSession.value.responses.push(fullResponse)
|
||
|
||
// 更新會話統計
|
||
updateSessionStats()
|
||
}
|
||
|
||
const completeReviewSession = () => {
|
||
if (!currentReviewSession.value) {
|
||
throw new Error('沒有活躍的複習會話')
|
||
}
|
||
|
||
currentReviewSession.value.endTime = new Date()
|
||
|
||
// 更新複習數據
|
||
const reviewData = vocabularyReviewData.value.get(currentReviewSession.value.vocabularyId)
|
||
if (reviewData) {
|
||
const updatedData = algorithm.calculateNextReview(reviewData, currentReviewSession.value)
|
||
vocabularyReviewData.value.set(reviewData.id, updatedData)
|
||
}
|
||
|
||
// 保存到歷史記錄
|
||
reviewHistory.value.push({ ...currentReviewSession.value })
|
||
|
||
// 清空當前會話
|
||
currentReviewSession.value = null
|
||
|
||
// 重新生成學習計劃
|
||
generateLearningPlan()
|
||
}
|
||
|
||
const updateSessionStats = () => {
|
||
if (!currentReviewSession.value) return
|
||
|
||
const responses = currentReviewSession.value.responses
|
||
const correctCount = responses.filter(r => r.isCorrect).length
|
||
const totalResponseTime = responses.reduce((sum, r) => sum + r.responseTime, 0)
|
||
|
||
currentReviewSession.value.overallAccuracy = responses.length > 0 ? correctCount / responses.length : 0
|
||
currentReviewSession.value.averageResponseTime = responses.length > 0 ? totalResponseTime / responses.length : 0
|
||
}
|
||
|
||
const generateLearningPlan = (daysAhead: number = 7) => {
|
||
const allVocabulary = Array.from(vocabularyReviewData.value.values())
|
||
const planMap = SpacedRepetitionAlgorithm.generateLearningPlan(allVocabulary, daysAhead)
|
||
|
||
learningPlan.value.clear()
|
||
|
||
planMap.forEach((vocabularyList, date) => {
|
||
const estimatedTime = vocabularyList.length * 2 // 每個詞彙平均2分鐘
|
||
|
||
learningPlan.value.set(date, {
|
||
date,
|
||
vocabulary: vocabularyList,
|
||
totalCount: vocabularyList.length,
|
||
estimatedTime
|
||
})
|
||
})
|
||
}
|
||
|
||
const calculateWeeklyStreak = (): number => {
|
||
if (reviewHistory.value.length === 0) return 0
|
||
|
||
const today = new Date()
|
||
let streak = 0
|
||
|
||
// 從今天開始往前檢查
|
||
for (let i = 0; i < 7; i++) {
|
||
const checkDate = new Date(today)
|
||
checkDate.setDate(today.getDate() - i)
|
||
checkDate.setHours(0, 0, 0, 0)
|
||
|
||
const nextDay = new Date(checkDate)
|
||
nextDay.setDate(checkDate.getDate() + 1)
|
||
|
||
const hasReviewOnDate = reviewHistory.value.some(session => {
|
||
const sessionDate = new Date(session.startTime)
|
||
return sessionDate >= checkDate && sessionDate < nextDay
|
||
})
|
||
|
||
if (hasReviewOnDate) {
|
||
streak++
|
||
} else if (i > 0) { // 今天沒複習不算打斷,其他天沒複習就算打斷
|
||
break
|
||
}
|
||
}
|
||
|
||
return streak
|
||
}
|
||
|
||
const getNextReviewTime = (): Date | null => {
|
||
const allVocabulary = Array.from(vocabularyReviewData.value.values())
|
||
if (allVocabulary.length === 0) return null
|
||
|
||
const nextReviews = allVocabulary
|
||
.filter(v => v.nextReviewDate > new Date())
|
||
.sort((a, b) => a.nextReviewDate.getTime() - b.nextReviewDate.getTime())
|
||
|
||
return nextReviews.length > 0 ? nextReviews[0].nextReviewDate : null
|
||
}
|
||
|
||
const getVocabularyReviewData = (vocabularyId: string): VocabularyReviewData | null => {
|
||
return vocabularyReviewData.value.get(vocabularyId) || null
|
||
}
|
||
|
||
const updateVocabularyReviewData = (data: VocabularyReviewData) => {
|
||
vocabularyReviewData.value.set(data.id, data)
|
||
}
|
||
|
||
const resetVocabularyProgress = (vocabularyId: string) => {
|
||
const defaultData = createDefaultVocabularyReviewData(vocabularyId)
|
||
vocabularyReviewData.value.set(vocabularyId, defaultData)
|
||
}
|
||
|
||
const getPersonalizedRecommendations = (): string[] => {
|
||
const recommendations: string[] = []
|
||
const stats = reviewStats.value
|
||
|
||
// 基於統計數據生成建議
|
||
if (stats.averageAccuracy < 0.7) {
|
||
recommendations.push('建議放慢學習節奏,專注於理解而不是數量')
|
||
}
|
||
|
||
if (stats.weeklyStreak === 0) {
|
||
recommendations.push('建立每日複習習慣,即使只複習5個詞彙也有幫助')
|
||
}
|
||
|
||
if (urgentReviewVocabulary.value.length > 10) {
|
||
recommendations.push('有較多詞彙需要緊急複習,建議優先處理過期詞彙')
|
||
}
|
||
|
||
if (stats.improvementTrend < 0) {
|
||
recommendations.push('學習效果有下降趨勢,建議調整學習策略或休息一下')
|
||
}
|
||
|
||
// 基於薄弱點生成建議
|
||
const topWeakness = weaknessAnalysis.value[0]
|
||
if (topWeakness) {
|
||
const weaknessRecommendations = {
|
||
spelling: '建議加強拼寫練習,可以嘗試手寫練習',
|
||
meaning: '建議多做詞義辨析練習,建立詞彙語義網絡',
|
||
pronunciation: '建議多聽音頻,模仿正確發音',
|
||
usage: '建議多閱讀例句,理解詞彙在不同語境中的用法',
|
||
grammar: '建議複習相關語法規則,理解詞彙的語法功能'
|
||
}
|
||
recommendations.push(weaknessRecommendations[topWeakness.type as keyof typeof weaknessRecommendations])
|
||
}
|
||
|
||
return recommendations.slice(0, 3) // 最多返回3個建議
|
||
}
|
||
|
||
const exportReviewData = () => {
|
||
return {
|
||
vocabularyReviewData: Object.fromEntries(vocabularyReviewData.value),
|
||
reviewHistory: reviewHistory.value,
|
||
exportDate: new Date().toISOString()
|
||
}
|
||
}
|
||
|
||
const importReviewData = (data: any) => {
|
||
if (data.vocabularyReviewData) {
|
||
vocabularyReviewData.value = new Map(Object.entries(data.vocabularyReviewData))
|
||
}
|
||
if (data.reviewHistory) {
|
||
reviewHistory.value = data.reviewHistory.map((session: any) => ({
|
||
...session,
|
||
startTime: new Date(session.startTime),
|
||
endTime: session.endTime ? new Date(session.endTime) : undefined,
|
||
responses: session.responses.map((response: any) => ({
|
||
...response,
|
||
timestamp: new Date(response.timestamp)
|
||
}))
|
||
}))
|
||
}
|
||
generateLearningPlan()
|
||
}
|
||
|
||
// 初始化
|
||
generateLearningPlan()
|
||
|
||
return {
|
||
// 狀態
|
||
vocabularyReviewData,
|
||
reviewHistory,
|
||
currentReviewSession,
|
||
learningPlan,
|
||
isLoading,
|
||
|
||
// 計算屬性
|
||
todaysReviewVocabulary,
|
||
reviewStats,
|
||
urgentReviewVocabulary,
|
||
weaknessAnalysis,
|
||
|
||
// 方法
|
||
initializeVocabularyReviewData,
|
||
startReviewSession,
|
||
addReviewResponse,
|
||
completeReviewSession,
|
||
generateLearningPlan,
|
||
getVocabularyReviewData,
|
||
updateVocabularyReviewData,
|
||
resetVocabularyProgress,
|
||
getPersonalizedRecommendations,
|
||
exportReviewData,
|
||
importReviewData
|
||
}
|
||
}) |