init
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import 'package:ai_english_learning/core/network/api_client.dart';
|
||||
import 'package:ai_english_learning/features/vocabulary/models/learning_session_model.dart';
|
||||
import 'package:ai_english_learning/features/vocabulary/models/word_model.dart';
|
||||
|
||||
class LearningService {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
LearningService({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||
|
||||
/// 开始学习会话
|
||||
Future<Map<String, dynamic>> startLearning(String bookId, int dailyGoal) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/books/$bookId/learn',
|
||||
data: {
|
||||
'dailyGoal': dailyGoal,
|
||||
},
|
||||
);
|
||||
|
||||
final data = response.data['data'];
|
||||
return {
|
||||
'session': LearningSession.fromJson(data['session']),
|
||||
'tasks': DailyLearningTasks.fromJson(data['tasks']),
|
||||
};
|
||||
} catch (e) {
|
||||
print('开始学习失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取今日学习任务
|
||||
Future<DailyLearningTasks> getTodayTasks(String bookId, {
|
||||
int newWords = 20,
|
||||
int review = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/books/$bookId/tasks',
|
||||
queryParameters: {
|
||||
'newWords': newWords,
|
||||
'review': review,
|
||||
},
|
||||
);
|
||||
|
||||
return DailyLearningTasks.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
print('获取学习任务失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交单词学习结果
|
||||
Future<UserWordProgress> submitWordStudy(
|
||||
String wordId,
|
||||
StudyDifficulty difficulty, {
|
||||
int? sessionId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/words/$wordId/study',
|
||||
data: {
|
||||
'difficulty': difficulty.name,
|
||||
if (sessionId != null) 'sessionId': sessionId,
|
||||
},
|
||||
);
|
||||
|
||||
return UserWordProgress.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
print('提交学习结果失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习统计
|
||||
Future<LearningStatistics> getLearningStatistics(String bookId) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/books/$bookId/statistics',
|
||||
);
|
||||
|
||||
return LearningStatistics.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
print('获取学习统计失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../models/learning_stats_model.dart';
|
||||
|
||||
/// 学习统计服务
|
||||
class LearningStatsService {
|
||||
final ApiClient _apiClient;
|
||||
final StorageService _storageService;
|
||||
|
||||
static const String _statsKey = 'learning_stats';
|
||||
static const String _achievementsKey = 'achievements';
|
||||
static const String _dailyRecordsKey = 'daily_records';
|
||||
|
||||
LearningStatsService({
|
||||
required ApiClient apiClient,
|
||||
required StorageService storageService,
|
||||
}) : _apiClient = apiClient,
|
||||
_storageService = storageService;
|
||||
|
||||
/// 获取用户学习统计
|
||||
Future<LearningStats> getUserStats() async {
|
||||
try {
|
||||
// 1. 从后端API获取每日、每周、每月统计数据
|
||||
final today = DateTime.now();
|
||||
final sevenDaysAgo = today.subtract(const Duration(days: 7));
|
||||
final thirtyDaysAgo = today.subtract(const Duration(days: 30));
|
||||
|
||||
final startDateStr = thirtyDaysAgo.toIso8601String().split('T')[0];
|
||||
final endDateStr = today.toIso8601String().split('T')[0];
|
||||
|
||||
print('🔍 开始获取学习统计: startDate=$startDateStr, endDate=$endDateStr');
|
||||
print('🔍 请求路径: /vocabulary/study/statistics/history');
|
||||
|
||||
// 获取历史统计数据
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/statistics/history',
|
||||
queryParameters: {
|
||||
'startDate': startDateStr,
|
||||
'endDate': endDateStr,
|
||||
},
|
||||
);
|
||||
|
||||
print('✅ API响应成功: statusCode=${response.statusCode}');
|
||||
|
||||
final List<dynamic> historyData = response.data['data'] ?? [];
|
||||
final dailyRecords = historyData.map((item) => DailyStudyRecord.fromJson(item)).toList();
|
||||
|
||||
print('📊 获取到 ${dailyRecords.length} 天的学习记录');
|
||||
|
||||
// 2. 计算总体统计
|
||||
final totalWordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
|
||||
final totalWordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
|
||||
final totalStudyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
|
||||
final totalExp = dailyRecords.fold(0, (sum, record) => sum + record.expGained);
|
||||
|
||||
// 3. 计算连续学习天数
|
||||
final currentStreak = _calculateCurrentStreak(dailyRecords);
|
||||
final maxStreak = _calculateMaxStreak(dailyRecords);
|
||||
|
||||
// 4. 计算周统计(使用最近7天的记录)
|
||||
final weeklyRecords = dailyRecords.where((record) {
|
||||
return record.date.isAfter(sevenDaysAgo.subtract(const Duration(days: 1)));
|
||||
}).toList();
|
||||
final weeklyStats = _calculateWeeklyStats(weeklyRecords);
|
||||
|
||||
print('📅 本周学习记录: ${weeklyRecords.length} 天, 学习 ${weeklyStats.wordsLearned} 个单词');
|
||||
|
||||
// 5. 计算月统计(使用最近30天的记录)
|
||||
final monthlyRecords = dailyRecords;
|
||||
|
||||
// 计算本月的周统计记录(用于柱状图)
|
||||
final weeklyStatsList = <WeeklyStats>[];
|
||||
final firstDayOfMonth = DateTime(today.year, today.month, 1);
|
||||
DateTime currentWeekStart = firstDayOfMonth;
|
||||
|
||||
while (currentWeekStart.isBefore(today)) {
|
||||
final weekEnd = currentWeekStart.add(const Duration(days: 6));
|
||||
final weekRecords = dailyRecords.where((record) {
|
||||
return record.date.isAfter(currentWeekStart.subtract(const Duration(days: 1))) &&
|
||||
record.date.isBefore(weekEnd.add(const Duration(days: 1)));
|
||||
}).toList();
|
||||
|
||||
if (weekRecords.isNotEmpty || currentWeekStart.isBefore(today)) {
|
||||
weeklyStatsList.add(_calculateWeeklyStats(weekRecords));
|
||||
}
|
||||
|
||||
currentWeekStart = currentWeekStart.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
final monthlyStats = _calculateMonthlyStats(monthlyRecords, weeklyStatsList);
|
||||
|
||||
print('📆 本月学习记录: ${monthlyRecords.length} 天, 学习 ${monthlyStats.wordsLearned} 个单词, ${weeklyStatsList.length} 周数据');
|
||||
|
||||
// 6. 计算等级和经验值
|
||||
final level = calculateLevel(totalExp);
|
||||
final nextLevelExp = calculateNextLevelExp(level);
|
||||
final currentExp = calculateCurrentLevelExp(totalExp, level);
|
||||
|
||||
// 7. 计算平均值
|
||||
final studyDays = dailyRecords.length;
|
||||
final averageDailyWords = studyDays > 0 ? totalWordsLearned / studyDays : 0.0;
|
||||
final averageDailyMinutes = studyDays > 0 ? totalStudyTimeMinutes / studyDays : 0.0;
|
||||
|
||||
// 8. 计算准确率
|
||||
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
|
||||
final averageAccuracy = totalTests > 0
|
||||
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
|
||||
: 0.0;
|
||||
|
||||
final stats = LearningStats(
|
||||
userId: 'current_user',
|
||||
totalStudyDays: studyDays,
|
||||
currentStreak: currentStreak,
|
||||
maxStreak: maxStreak,
|
||||
totalWordsLearned: totalWordsLearned,
|
||||
totalWordsReviewed: totalWordsReviewed,
|
||||
totalStudyTimeMinutes: totalStudyTimeMinutes,
|
||||
averageDailyWords: averageDailyWords,
|
||||
averageDailyMinutes: averageDailyMinutes,
|
||||
accuracyRate: averageAccuracy,
|
||||
completedBooks: 0,
|
||||
currentBooks: 0,
|
||||
masteredWords: 0,
|
||||
learningWords: 0,
|
||||
reviewWords: 0,
|
||||
weeklyStats: weeklyStats,
|
||||
monthlyStats: monthlyStats,
|
||||
level: level,
|
||||
currentExp: currentExp,
|
||||
nextLevelExp: nextLevelExp,
|
||||
lastStudyTime: dailyRecords.isNotEmpty ? dailyRecords.last.date : null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
dailyRecords: dailyRecords,
|
||||
achievements: [],
|
||||
leaderboard: null,
|
||||
);
|
||||
|
||||
// 保存到本地缓存
|
||||
await _saveStatsToLocal(stats);
|
||||
|
||||
return stats;
|
||||
} catch (e, stackTrace) {
|
||||
print('❌ 从API获取学习统计失败: $e');
|
||||
print('❌ 错误堆栈: $stackTrace');
|
||||
|
||||
// 如果API失败,尝试从本地缓存加载
|
||||
final localData = await _storageService.getString(_statsKey);
|
||||
if (localData != null) {
|
||||
print('💾 从本地缓存加载数据');
|
||||
return LearningStats.fromJson(json.decode(localData));
|
||||
}
|
||||
// 如果本地也没有,返回默认数据
|
||||
print('⚠️ 返回默认数据');
|
||||
return _createDefaultStats();
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新学习统计
|
||||
Future<LearningStats> updateStats({
|
||||
required int wordsLearned,
|
||||
required int wordsReviewed,
|
||||
required int studyTimeMinutes,
|
||||
required double accuracyRate,
|
||||
List<String> vocabularyBookIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final currentStats = await getUserStats();
|
||||
final today = DateTime.now();
|
||||
|
||||
// 更新每日记录
|
||||
await _updateDailyRecord(
|
||||
date: today,
|
||||
wordsLearned: wordsLearned,
|
||||
wordsReviewed: wordsReviewed,
|
||||
studyTimeMinutes: studyTimeMinutes,
|
||||
accuracyRate: accuracyRate,
|
||||
vocabularyBookIds: vocabularyBookIds,
|
||||
);
|
||||
|
||||
// 计算新的统计数据
|
||||
final updatedStats = await _calculateUpdatedStats(currentStats);
|
||||
|
||||
// 保存到本地
|
||||
await _saveStatsToLocal(updatedStats);
|
||||
|
||||
return updatedStats;
|
||||
} catch (e) {
|
||||
throw Exception('更新学习统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取每日学习记录
|
||||
Future<List<DailyStudyRecord>> getDailyRecords({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
try {
|
||||
final localData = await _storageService.getString(_dailyRecordsKey);
|
||||
if (localData == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final Map<String, dynamic> recordsMap = json.decode(localData);
|
||||
final records = recordsMap.values
|
||||
.map((json) => DailyStudyRecord.fromJson(json))
|
||||
.toList();
|
||||
|
||||
// 按日期过滤
|
||||
if (startDate != null || endDate != null) {
|
||||
return records.where((record) {
|
||||
if (startDate != null && record.date.isBefore(startDate)) {
|
||||
return false;
|
||||
}
|
||||
if (endDate != null && record.date.isAfter(endDate)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return records;
|
||||
} catch (e) {
|
||||
throw Exception('获取每日记录失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取周统计
|
||||
Future<WeeklyStats> getWeeklyStats([DateTime? weekStart]) async {
|
||||
try {
|
||||
final startDate = weekStart ?? _getWeekStart(DateTime.now());
|
||||
final endDate = startDate.add(const Duration(days: 6));
|
||||
|
||||
final dailyRecords = await getDailyRecords(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
|
||||
return _calculateWeeklyStats(dailyRecords);
|
||||
} catch (e) {
|
||||
throw Exception('获取周统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取月统计
|
||||
Future<MonthlyStats> getMonthlyStats([DateTime? month]) async {
|
||||
try {
|
||||
final targetMonth = month ?? DateTime.now();
|
||||
final startDate = DateTime(targetMonth.year, targetMonth.month, 1);
|
||||
final endDate = DateTime(targetMonth.year, targetMonth.month + 1, 0);
|
||||
|
||||
final dailyRecords = await getDailyRecords(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
|
||||
// 计算周统计
|
||||
final weeklyRecords = <WeeklyStats>[];
|
||||
DateTime currentWeekStart = startDate;
|
||||
|
||||
while (currentWeekStart.isBefore(endDate)) {
|
||||
final weekEnd = currentWeekStart.add(const Duration(days: 6));
|
||||
final weekRecords = dailyRecords.where((record) {
|
||||
return record.date.isAfter(currentWeekStart.subtract(const Duration(days: 1))) &&
|
||||
record.date.isBefore(weekEnd.add(const Duration(days: 1)));
|
||||
}).toList();
|
||||
|
||||
weeklyRecords.add(_calculateWeeklyStats(weekRecords));
|
||||
currentWeekStart = currentWeekStart.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
return _calculateMonthlyStats(dailyRecords, weeklyRecords);
|
||||
} catch (e) {
|
||||
throw Exception('获取月统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户成就
|
||||
Future<List<Achievement>> getUserAchievements() async {
|
||||
try {
|
||||
final localData = await _storageService.getString(_achievementsKey);
|
||||
if (localData != null) {
|
||||
final List<dynamic> jsonList = json.decode(localData);
|
||||
return jsonList.map((json) => Achievement.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
// 返回默认成就列表
|
||||
return _createDefaultAchievements();
|
||||
} catch (e) {
|
||||
throw Exception('获取成就失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查并解锁成就
|
||||
Future<List<Achievement>> checkAndUnlockAchievements() async {
|
||||
try {
|
||||
final stats = await getUserStats();
|
||||
final achievements = await getUserAchievements();
|
||||
final unlockedAchievements = <Achievement>[];
|
||||
|
||||
for (final achievement in achievements) {
|
||||
if (!achievement.isUnlocked) {
|
||||
final shouldUnlock = _checkAchievementCondition(achievement, stats);
|
||||
if (shouldUnlock) {
|
||||
final unlockedAchievement = achievement.copyWith(
|
||||
isUnlocked: true,
|
||||
unlockedAt: DateTime.now(),
|
||||
);
|
||||
unlockedAchievements.add(unlockedAchievement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unlockedAchievements.isNotEmpty) {
|
||||
// 更新成就列表
|
||||
final updatedAchievements = achievements.map((achievement) {
|
||||
final unlocked = unlockedAchievements
|
||||
.where((a) => a.id == achievement.id)
|
||||
.firstOrNull;
|
||||
return unlocked ?? achievement;
|
||||
}).toList();
|
||||
|
||||
await _saveAchievementsToLocal(updatedAchievements);
|
||||
}
|
||||
|
||||
return unlockedAchievements;
|
||||
} catch (e) {
|
||||
throw Exception('检查成就失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取排行榜
|
||||
Future<Leaderboard> getLeaderboard({
|
||||
required LeaderboardType type,
|
||||
required LeaderboardPeriod period,
|
||||
}) async {
|
||||
try {
|
||||
// 模拟排行榜数据
|
||||
final entries = _generateMockLeaderboardEntries(type);
|
||||
|
||||
return Leaderboard(
|
||||
type: type,
|
||||
period: period,
|
||||
entries: entries,
|
||||
userRank: Random().nextInt(100) + 1,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('获取排行榜失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算用户等级
|
||||
int calculateLevel(int totalExp) {
|
||||
// 等级计算公式:level = floor(sqrt(totalExp / 100))
|
||||
return (sqrt(totalExp / 100)).floor() + 1;
|
||||
}
|
||||
|
||||
/// 计算升级所需经验值
|
||||
int calculateNextLevelExp(int level) {
|
||||
// 下一级所需总经验值:(level^2) * 100
|
||||
return level * level * 100;
|
||||
}
|
||||
|
||||
/// 计算当前等级经验值
|
||||
int calculateCurrentLevelExp(int totalExp, int level) {
|
||||
final currentLevelTotalExp = (level - 1) * (level - 1) * 100;
|
||||
return totalExp - currentLevelTotalExp;
|
||||
}
|
||||
|
||||
/// 更新每日记录
|
||||
Future<void> _updateDailyRecord({
|
||||
required DateTime date,
|
||||
required int wordsLearned,
|
||||
required int wordsReviewed,
|
||||
required int studyTimeMinutes,
|
||||
required double accuracyRate,
|
||||
required List<String> vocabularyBookIds,
|
||||
}) async {
|
||||
final dateKey = _formatDateKey(date);
|
||||
final recordsData = await _storageService.getString(_dailyRecordsKey);
|
||||
|
||||
Map<String, dynamic> records = {};
|
||||
if (recordsData != null) {
|
||||
records = json.decode(recordsData);
|
||||
}
|
||||
|
||||
// 获取现有记录或创建新记录
|
||||
DailyStudyRecord existingRecord;
|
||||
if (records.containsKey(dateKey)) {
|
||||
existingRecord = DailyStudyRecord.fromJson(records[dateKey]);
|
||||
} else {
|
||||
existingRecord = DailyStudyRecord(
|
||||
date: DateTime(date.year, date.month, date.day),
|
||||
wordsLearned: 0,
|
||||
wordsReviewed: 0,
|
||||
studyTimeMinutes: 0,
|
||||
accuracyRate: 0.0,
|
||||
testsCompleted: 0,
|
||||
expGained: 0,
|
||||
vocabularyBookIds: [],
|
||||
);
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
final updatedRecord = DailyStudyRecord(
|
||||
date: existingRecord.date,
|
||||
wordsLearned: existingRecord.wordsLearned + wordsLearned,
|
||||
wordsReviewed: existingRecord.wordsReviewed + wordsReviewed,
|
||||
studyTimeMinutes: existingRecord.studyTimeMinutes + studyTimeMinutes,
|
||||
accuracyRate: (existingRecord.accuracyRate + accuracyRate) / 2,
|
||||
testsCompleted: existingRecord.testsCompleted + 1,
|
||||
expGained: existingRecord.expGained + _calculateExpGained(wordsLearned, wordsReviewed, accuracyRate),
|
||||
vocabularyBookIds: [...existingRecord.vocabularyBookIds, ...vocabularyBookIds].toSet().toList(),
|
||||
);
|
||||
|
||||
records[dateKey] = updatedRecord.toJson();
|
||||
await _storageService.setString(_dailyRecordsKey, json.encode(records));
|
||||
}
|
||||
|
||||
/// 计算更新后的统计数据
|
||||
Future<LearningStats> _calculateUpdatedStats(LearningStats currentStats) async {
|
||||
final allRecords = await getDailyRecords();
|
||||
|
||||
// 计算总数据
|
||||
final totalWordsLearned = allRecords.fold(0, (sum, record) => sum + record.wordsLearned);
|
||||
final totalWordsReviewed = allRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
|
||||
final totalStudyTimeMinutes = allRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
|
||||
final totalExp = allRecords.fold(0, (sum, record) => sum + record.expGained);
|
||||
|
||||
// 计算连续学习天数
|
||||
final currentStreak = _calculateCurrentStreak(allRecords);
|
||||
final maxStreak = _calculateMaxStreak(allRecords);
|
||||
|
||||
// 计算平均值
|
||||
final studyDays = allRecords.length;
|
||||
final averageDailyWords = studyDays > 0 ? totalWordsLearned / studyDays : 0.0;
|
||||
final averageDailyMinutes = studyDays > 0 ? totalStudyTimeMinutes / studyDays : 0.0;
|
||||
|
||||
// 计算准确率
|
||||
final totalTests = allRecords.fold(0, (sum, record) => sum + record.testsCompleted);
|
||||
final averageAccuracy = totalTests > 0
|
||||
? allRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
|
||||
: 0.0;
|
||||
|
||||
// 计算等级
|
||||
final level = calculateLevel(totalExp);
|
||||
final nextLevelExp = calculateNextLevelExp(level);
|
||||
final currentExp = calculateCurrentLevelExp(totalExp, level);
|
||||
|
||||
// 获取周月统计
|
||||
final weeklyStats = await getWeeklyStats();
|
||||
final monthlyStats = await getMonthlyStats();
|
||||
|
||||
return currentStats.copyWith(
|
||||
totalStudyDays: studyDays,
|
||||
currentStreak: currentStreak,
|
||||
maxStreak: maxStreak > currentStats.maxStreak ? maxStreak : currentStats.maxStreak,
|
||||
totalWordsLearned: totalWordsLearned,
|
||||
totalWordsReviewed: totalWordsReviewed,
|
||||
totalStudyTimeMinutes: totalStudyTimeMinutes,
|
||||
averageDailyWords: averageDailyWords,
|
||||
averageDailyMinutes: averageDailyMinutes,
|
||||
accuracyRate: averageAccuracy,
|
||||
weeklyStats: weeklyStats,
|
||||
monthlyStats: monthlyStats,
|
||||
level: level,
|
||||
currentExp: currentExp,
|
||||
nextLevelExp: nextLevelExp,
|
||||
lastStudyTime: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建默认统计数据
|
||||
LearningStats _createDefaultStats() {
|
||||
final now = DateTime.now();
|
||||
return LearningStats(
|
||||
userId: 'current_user',
|
||||
totalStudyDays: 0,
|
||||
currentStreak: 0,
|
||||
maxStreak: 0,
|
||||
totalWordsLearned: 0,
|
||||
totalWordsReviewed: 0,
|
||||
totalStudyTimeMinutes: 0,
|
||||
averageDailyWords: 0.0,
|
||||
averageDailyMinutes: 0.0,
|
||||
accuracyRate: 0.0,
|
||||
completedBooks: 0,
|
||||
currentBooks: 0,
|
||||
masteredWords: 0,
|
||||
learningWords: 0,
|
||||
reviewWords: 0,
|
||||
weeklyStats: WeeklyStats(
|
||||
studyDays: 0,
|
||||
wordsLearned: 0,
|
||||
wordsReviewed: 0,
|
||||
studyTimeMinutes: 0,
|
||||
accuracyRate: 0.0,
|
||||
dailyRecords: [],
|
||||
),
|
||||
monthlyStats: MonthlyStats(
|
||||
studyDays: 0,
|
||||
wordsLearned: 0,
|
||||
wordsReviewed: 0,
|
||||
studyTimeMinutes: 0,
|
||||
accuracyRate: 0.0,
|
||||
completedBooks: 0,
|
||||
weeklyRecords: [],
|
||||
),
|
||||
level: 1,
|
||||
currentExp: 0,
|
||||
nextLevelExp: 100,
|
||||
lastStudyTime: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
dailyRecords: [],
|
||||
achievements: [],
|
||||
leaderboard: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建默认成就列表
|
||||
List<Achievement> _createDefaultAchievements() {
|
||||
return [
|
||||
Achievement(
|
||||
id: 'first_word',
|
||||
name: '初学者',
|
||||
description: '学习第一个单词',
|
||||
icon: '🌱',
|
||||
type: AchievementType.wordsLearned,
|
||||
isUnlocked: false,
|
||||
progress: 0,
|
||||
target: 1,
|
||||
rewardExp: 10,
|
||||
),
|
||||
Achievement(
|
||||
id: 'hundred_words',
|
||||
name: '百词斩',
|
||||
description: '累计学习100个单词',
|
||||
icon: '💯',
|
||||
type: AchievementType.wordsLearned,
|
||||
isUnlocked: false,
|
||||
progress: 0,
|
||||
target: 100,
|
||||
rewardExp: 100,
|
||||
),
|
||||
Achievement(
|
||||
id: 'first_streak',
|
||||
name: '坚持不懈',
|
||||
description: '连续学习7天',
|
||||
icon: '🔥',
|
||||
type: AchievementType.streak,
|
||||
isUnlocked: false,
|
||||
progress: 0,
|
||||
target: 7,
|
||||
rewardExp: 50,
|
||||
),
|
||||
Achievement(
|
||||
id: 'month_streak',
|
||||
name: '月度达人',
|
||||
description: '连续学习30天',
|
||||
icon: '🏆',
|
||||
type: AchievementType.streak,
|
||||
isUnlocked: false,
|
||||
progress: 0,
|
||||
target: 30,
|
||||
rewardExp: 300,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// 计算周统计
|
||||
WeeklyStats _calculateWeeklyStats(List<DailyStudyRecord> dailyRecords) {
|
||||
final studyDays = dailyRecords.length;
|
||||
final wordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
|
||||
final wordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
|
||||
final studyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
|
||||
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
|
||||
final accuracyRate = totalTests > 0
|
||||
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
|
||||
: 0.0;
|
||||
|
||||
return WeeklyStats(
|
||||
studyDays: studyDays,
|
||||
wordsLearned: wordsLearned,
|
||||
wordsReviewed: wordsReviewed,
|
||||
studyTimeMinutes: studyTimeMinutes,
|
||||
accuracyRate: accuracyRate,
|
||||
dailyRecords: dailyRecords,
|
||||
);
|
||||
}
|
||||
|
||||
/// 计算月统计
|
||||
MonthlyStats _calculateMonthlyStats(
|
||||
List<DailyStudyRecord> dailyRecords,
|
||||
List<WeeklyStats> weeklyRecords,
|
||||
) {
|
||||
final studyDays = dailyRecords.length;
|
||||
final wordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
|
||||
final wordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
|
||||
final studyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
|
||||
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
|
||||
final accuracyRate = totalTests > 0
|
||||
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
|
||||
: 0.0;
|
||||
|
||||
return MonthlyStats(
|
||||
studyDays: studyDays,
|
||||
wordsLearned: wordsLearned,
|
||||
wordsReviewed: wordsReviewed,
|
||||
studyTimeMinutes: studyTimeMinutes,
|
||||
accuracyRate: accuracyRate,
|
||||
completedBooks: 0, // TODO: 从实际数据计算
|
||||
weeklyRecords: weeklyRecords,
|
||||
);
|
||||
}
|
||||
|
||||
/// 计算当前连续学习天数
|
||||
int _calculateCurrentStreak(List<DailyStudyRecord> records) {
|
||||
if (records.isEmpty) return 0;
|
||||
|
||||
records.sort((a, b) => b.date.compareTo(a.date));
|
||||
|
||||
int streak = 0;
|
||||
DateTime currentDate = DateTime.now();
|
||||
|
||||
for (final record in records) {
|
||||
final daysDiff = currentDate.difference(record.date).inDays;
|
||||
|
||||
if (daysDiff == streak) {
|
||||
streak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
/// 计算最大连续学习天数
|
||||
int _calculateMaxStreak(List<DailyStudyRecord> records) {
|
||||
if (records.isEmpty) return 0;
|
||||
|
||||
records.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
int maxStreak = 1;
|
||||
int currentStreak = 1;
|
||||
|
||||
for (int i = 1; i < records.length; i++) {
|
||||
final daysDiff = records[i].date.difference(records[i - 1].date).inDays;
|
||||
|
||||
if (daysDiff == 1) {
|
||||
currentStreak++;
|
||||
maxStreak = maxStreak > currentStreak ? maxStreak : currentStreak;
|
||||
} else {
|
||||
currentStreak = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return maxStreak;
|
||||
}
|
||||
|
||||
/// 计算获得的经验值
|
||||
int _calculateExpGained(int wordsLearned, int wordsReviewed, double accuracyRate) {
|
||||
final baseExp = wordsLearned * 2 + wordsReviewed * 1;
|
||||
final accuracyBonus = (accuracyRate * baseExp * 0.5).round();
|
||||
return baseExp + accuracyBonus;
|
||||
}
|
||||
|
||||
/// 检查成就条件
|
||||
bool _checkAchievementCondition(Achievement achievement, LearningStats stats) {
|
||||
switch (achievement.type) {
|
||||
case AchievementType.wordsLearned:
|
||||
return stats.totalWordsLearned >= achievement.target;
|
||||
case AchievementType.streak:
|
||||
return stats.currentStreak >= achievement.target;
|
||||
case AchievementType.studyDays:
|
||||
return stats.totalStudyDays >= achievement.target;
|
||||
case AchievementType.booksCompleted:
|
||||
return stats.completedBooks >= achievement.target;
|
||||
case AchievementType.studyTime:
|
||||
return stats.totalStudyTimeMinutes >= achievement.target;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成模拟排行榜数据
|
||||
List<LeaderboardEntry> _generateMockLeaderboardEntries(LeaderboardType type) {
|
||||
final random = Random();
|
||||
final entries = <LeaderboardEntry>[];
|
||||
|
||||
for (int i = 1; i <= 50; i++) {
|
||||
entries.add(LeaderboardEntry(
|
||||
rank: i,
|
||||
userId: 'user_$i',
|
||||
username: '用户$i',
|
||||
score: random.nextInt(1000) + 100,
|
||||
level: random.nextInt(20) + 1,
|
||||
));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// 获取周开始日期
|
||||
DateTime _getWeekStart(DateTime date) {
|
||||
final weekday = date.weekday;
|
||||
return date.subtract(Duration(days: weekday - 1));
|
||||
}
|
||||
|
||||
/// 格式化日期键
|
||||
String _formatDateKey(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 保存统计数据到本地
|
||||
Future<void> _saveStatsToLocal(LearningStats stats) async {
|
||||
await _storageService.setString(_statsKey, json.encode(stats.toJson()));
|
||||
}
|
||||
|
||||
/// 保存成就到本地
|
||||
Future<void> _saveAchievementsToLocal(List<Achievement> achievements) async {
|
||||
final jsonList = achievements.map((achievement) => achievement.toJson()).toList();
|
||||
await _storageService.setString(_achievementsKey, json.encode(jsonList));
|
||||
}
|
||||
}
|
||||
442
client/lib/features/vocabulary/services/study_plan_service.dart
Normal file
442
client/lib/features/vocabulary/services/study_plan_service.dart
Normal file
@@ -0,0 +1,442 @@
|
||||
import 'dart:convert';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../models/study_plan_model.dart';
|
||||
import '../models/vocabulary_book_model.dart';
|
||||
|
||||
/// 学习计划服务
|
||||
class StudyPlanService {
|
||||
final ApiClient _apiClient;
|
||||
final StorageService _storageService;
|
||||
|
||||
static const String _studyPlansKey = 'study_plans';
|
||||
static const String _dailyRecordsKey = 'daily_study_records';
|
||||
static const String _templatesKey = 'study_plan_templates';
|
||||
|
||||
StudyPlanService({
|
||||
required ApiClient apiClient,
|
||||
required StorageService storageService,
|
||||
}) : _apiClient = apiClient,
|
||||
_storageService = storageService;
|
||||
|
||||
/// 获取用户的所有学习计划
|
||||
Future<List<StudyPlan>> getUserStudyPlans() async {
|
||||
try {
|
||||
final localData = await _storageService.getString(_studyPlansKey);
|
||||
if (localData != null) {
|
||||
final List<dynamic> jsonList = json.decode(localData);
|
||||
return jsonList.map((json) => StudyPlan.fromJson(json)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
throw Exception('获取学习计划失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建新的学习计划
|
||||
Future<StudyPlan> createStudyPlan({
|
||||
required String name,
|
||||
String? description,
|
||||
required StudyPlanType type,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
required int dailyTarget,
|
||||
List<String> vocabularyBookIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final studyPlan = StudyPlan(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
name: name,
|
||||
description: description,
|
||||
userId: 'current_user',
|
||||
type: type,
|
||||
status: StudyPlanStatus.active,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
dailyTarget: dailyTarget,
|
||||
vocabularyBookIds: vocabularyBookIds,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final existingPlans = await getUserStudyPlans();
|
||||
existingPlans.add(studyPlan);
|
||||
await _saveStudyPlansToLocal(existingPlans);
|
||||
|
||||
return studyPlan;
|
||||
} catch (e) {
|
||||
throw Exception('创建学习计划失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新学习计划
|
||||
Future<StudyPlan> updateStudyPlan(StudyPlan studyPlan) async {
|
||||
try {
|
||||
final existingPlans = await getUserStudyPlans();
|
||||
final index = existingPlans.indexWhere((plan) => plan.id == studyPlan.id);
|
||||
|
||||
if (index == -1) {
|
||||
throw Exception('学习计划不存在');
|
||||
}
|
||||
|
||||
final updatedPlan = studyPlan.copyWith(updatedAt: DateTime.now());
|
||||
existingPlans[index] = updatedPlan;
|
||||
await _saveStudyPlansToLocal(existingPlans);
|
||||
|
||||
return updatedPlan;
|
||||
} catch (e) {
|
||||
throw Exception('更新学习计划失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除学习计划
|
||||
Future<void> deleteStudyPlan(String planId) async {
|
||||
try {
|
||||
final existingPlans = await getUserStudyPlans();
|
||||
existingPlans.removeWhere((plan) => plan.id == planId);
|
||||
await _saveStudyPlansToLocal(existingPlans);
|
||||
} catch (e) {
|
||||
throw Exception('删除学习计划失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录每日学习进度
|
||||
Future<void> recordDailyProgress({
|
||||
required String planId,
|
||||
required DateTime date,
|
||||
required int wordsLearned,
|
||||
required int wordsReviewed,
|
||||
required int studyTimeMinutes,
|
||||
List<String> vocabularyBookIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final record = DailyStudyRecord(
|
||||
date: DateTime(date.year, date.month, date.day),
|
||||
wordsLearned: wordsLearned,
|
||||
wordsReviewed: wordsReviewed,
|
||||
studyTimeMinutes: studyTimeMinutes,
|
||||
vocabularyBookIds: vocabularyBookIds,
|
||||
);
|
||||
|
||||
// 获取现有记录
|
||||
final allRecords = await _getAllDailyRecords();
|
||||
final recordKey = '${planId}_${_formatDateKey(date)}';
|
||||
|
||||
// 更新或添加记录
|
||||
allRecords[recordKey] = record;
|
||||
await _saveDailyRecordsToLocal(allRecords);
|
||||
|
||||
// 更新学习计划的进度
|
||||
await _updateStudyPlanProgress(planId);
|
||||
} catch (e) {
|
||||
throw Exception('记录学习进度失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习计划统计信息
|
||||
Future<StudyPlanStats> getStudyPlanStats(String planId) async {
|
||||
try {
|
||||
final studyPlans = await getUserStudyPlans();
|
||||
final studyPlan = studyPlans.where((plan) => plan.id == planId).firstOrNull;
|
||||
|
||||
if (studyPlan == null) {
|
||||
throw Exception('学习计划不存在');
|
||||
}
|
||||
|
||||
final allRecords = await _getAllDailyRecords();
|
||||
final planRecords = <DailyStudyRecord>[];
|
||||
|
||||
// 获取该计划的所有记录
|
||||
for (final entry in allRecords.entries) {
|
||||
if (entry.key.startsWith('${planId}_')) {
|
||||
planRecords.add(entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算统计信息
|
||||
final totalDays = studyPlan.endDate.difference(studyPlan.startDate).inDays + 1;
|
||||
final studiedDays = planRecords.length;
|
||||
final totalWordsLearned = planRecords.fold(0, (sum, record) => sum + record.wordsLearned);
|
||||
final totalWordsReviewed = planRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
|
||||
|
||||
// 计算连续学习天数
|
||||
final currentStreak = _calculateCurrentStreak(planRecords);
|
||||
final maxStreak = _calculateMaxStreak(planRecords);
|
||||
|
||||
final completionRate = studyPlan.totalWords > 0
|
||||
? studyPlan.completedWords / studyPlan.totalWords
|
||||
: 0.0;
|
||||
|
||||
final averageDailyWords = studiedDays > 0
|
||||
? totalWordsLearned / studiedDays
|
||||
: 0.0;
|
||||
|
||||
final remainingDays = studyPlan.endDate.difference(DateTime.now()).inDays;
|
||||
final remainingWords = studyPlan.totalWords - studyPlan.completedWords;
|
||||
|
||||
final lastStudyDate = planRecords.isNotEmpty
|
||||
? planRecords.map((r) => r.date).reduce((a, b) => a.isAfter(b) ? a : b)
|
||||
: null;
|
||||
|
||||
return StudyPlanStats(
|
||||
planId: planId,
|
||||
totalDays: totalDays,
|
||||
studiedDays: studiedDays,
|
||||
totalWords: studyPlan.totalWords,
|
||||
learnedWords: totalWordsLearned,
|
||||
reviewedWords: totalWordsReviewed,
|
||||
currentStreak: currentStreak,
|
||||
maxStreak: maxStreak,
|
||||
completionRate: completionRate,
|
||||
averageDailyWords: averageDailyWords,
|
||||
remainingDays: remainingDays > 0 ? remainingDays : 0,
|
||||
remainingWords: remainingWords > 0 ? remainingWords : 0,
|
||||
lastStudyDate: lastStudyDate,
|
||||
dailyRecords: planRecords,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('获取学习计划统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取今日学习任务
|
||||
Future<List<StudyPlan>> getTodayStudyTasks() async {
|
||||
try {
|
||||
final allPlans = await getUserStudyPlans();
|
||||
final today = DateTime.now();
|
||||
|
||||
return allPlans.where((plan) {
|
||||
return plan.status == StudyPlanStatus.active &&
|
||||
plan.startDate.isBefore(today.add(const Duration(days: 1))) &&
|
||||
plan.endDate.isAfter(today.subtract(const Duration(days: 1)));
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
throw Exception('获取今日学习任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查今日是否完成目标
|
||||
Future<bool> isTodayTargetAchieved(String planId) async {
|
||||
try {
|
||||
final today = DateTime.now();
|
||||
final allRecords = await _getAllDailyRecords();
|
||||
final recordKey = '${planId}_${_formatDateKey(today)}';
|
||||
|
||||
final todayRecord = allRecords[recordKey];
|
||||
if (todayRecord == null) return false;
|
||||
|
||||
final studyPlans = await getUserStudyPlans();
|
||||
final studyPlan = studyPlans.where((plan) => plan.id == planId).firstOrNull;
|
||||
|
||||
if (studyPlan == null) return false;
|
||||
|
||||
return todayRecord.wordsLearned >= studyPlan.dailyTarget;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习计划模板
|
||||
Future<List<StudyPlanTemplate>> getStudyPlanTemplates() async {
|
||||
try {
|
||||
// 返回预设的学习计划模板
|
||||
return [
|
||||
StudyPlanTemplate(
|
||||
id: 'daily_basic',
|
||||
name: '每日基础学习',
|
||||
description: '每天学习20个新单词,适合初学者',
|
||||
type: StudyPlanType.daily,
|
||||
durationDays: 30,
|
||||
dailyTarget: 20,
|
||||
difficulty: 1,
|
||||
tags: ['基础', '初学者'],
|
||||
isPopular: true,
|
||||
),
|
||||
StudyPlanTemplate(
|
||||
id: 'weekly_intensive',
|
||||
name: '周集中学习',
|
||||
description: '每周集中学习,每天50个单词',
|
||||
type: StudyPlanType.weekly,
|
||||
durationDays: 7,
|
||||
dailyTarget: 50,
|
||||
difficulty: 3,
|
||||
tags: ['集中', '高强度'],
|
||||
),
|
||||
StudyPlanTemplate(
|
||||
id: 'exam_prep_cet4',
|
||||
name: 'CET-4考试准备',
|
||||
description: '为大学英语四级考试准备的学习计划',
|
||||
type: StudyPlanType.examPrep,
|
||||
durationDays: 60,
|
||||
dailyTarget: 30,
|
||||
difficulty: 2,
|
||||
tags: ['考试', 'CET-4'],
|
||||
isPopular: true,
|
||||
),
|
||||
StudyPlanTemplate(
|
||||
id: 'exam_prep_cet6',
|
||||
name: 'CET-6考试准备',
|
||||
description: '为大学英语六级考试准备的学习计划',
|
||||
type: StudyPlanType.examPrep,
|
||||
durationDays: 90,
|
||||
dailyTarget: 40,
|
||||
difficulty: 3,
|
||||
tags: ['考试', 'CET-6'],
|
||||
),
|
||||
StudyPlanTemplate(
|
||||
id: 'toefl_prep',
|
||||
name: 'TOEFL考试准备',
|
||||
description: '为托福考试准备的高强度学习计划',
|
||||
type: StudyPlanType.examPrep,
|
||||
durationDays: 120,
|
||||
dailyTarget: 60,
|
||||
difficulty: 4,
|
||||
tags: ['考试', 'TOEFL', '出国'],
|
||||
isPopular: true,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('获取学习计划模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从模板创建学习计划
|
||||
Future<StudyPlan> createStudyPlanFromTemplate({
|
||||
required StudyPlanTemplate template,
|
||||
required DateTime startDate,
|
||||
List<String> vocabularyBookIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final endDate = startDate.add(Duration(days: template.durationDays - 1));
|
||||
|
||||
return await createStudyPlan(
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
type: template.type,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
dailyTarget: template.dailyTarget,
|
||||
vocabularyBookIds: vocabularyBookIds,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('从模板创建学习计划失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存学习计划到本地存储
|
||||
Future<void> _saveStudyPlansToLocal(List<StudyPlan> studyPlans) async {
|
||||
final jsonList = studyPlans.map((plan) => plan.toJson()).toList();
|
||||
await _storageService.setString(_studyPlansKey, json.encode(jsonList));
|
||||
}
|
||||
|
||||
/// 保存每日记录到本地存储
|
||||
Future<void> _saveDailyRecordsToLocal(Map<String, DailyStudyRecord> records) async {
|
||||
final jsonMap = records.map((key, record) => MapEntry(key, record.toJson()));
|
||||
await _storageService.setString(_dailyRecordsKey, json.encode(jsonMap));
|
||||
}
|
||||
|
||||
/// 获取所有每日记录
|
||||
Future<Map<String, DailyStudyRecord>> _getAllDailyRecords() async {
|
||||
final localData = await _storageService.getString(_dailyRecordsKey);
|
||||
if (localData == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonMap = json.decode(localData);
|
||||
return jsonMap.map((key, value) => MapEntry(key, DailyStudyRecord.fromJson(value)));
|
||||
}
|
||||
|
||||
/// 更新学习计划进度
|
||||
Future<void> _updateStudyPlanProgress(String planId) async {
|
||||
try {
|
||||
final studyPlans = await getUserStudyPlans();
|
||||
final index = studyPlans.indexWhere((plan) => plan.id == planId);
|
||||
|
||||
if (index == -1) return;
|
||||
|
||||
final studyPlan = studyPlans[index];
|
||||
final allRecords = await _getAllDailyRecords();
|
||||
|
||||
// 计算总完成单词数
|
||||
int totalCompleted = 0;
|
||||
for (final entry in allRecords.entries) {
|
||||
if (entry.key.startsWith('${planId}_')) {
|
||||
totalCompleted += entry.value.wordsLearned;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算连续学习天数
|
||||
final planRecords = <DailyStudyRecord>[];
|
||||
for (final entry in allRecords.entries) {
|
||||
if (entry.key.startsWith('${planId}_')) {
|
||||
planRecords.add(entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
final currentStreak = _calculateCurrentStreak(planRecords);
|
||||
final maxStreak = _calculateMaxStreak(planRecords);
|
||||
|
||||
final updatedPlan = studyPlan.copyWith(
|
||||
completedWords: totalCompleted,
|
||||
currentStreak: currentStreak,
|
||||
maxStreak: maxStreak > studyPlan.maxStreak ? maxStreak : studyPlan.maxStreak,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
studyPlans[index] = updatedPlan;
|
||||
await _saveStudyPlansToLocal(studyPlans);
|
||||
} catch (e) {
|
||||
// 忽略更新错误
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算当前连续学习天数
|
||||
int _calculateCurrentStreak(List<DailyStudyRecord> records) {
|
||||
if (records.isEmpty) return 0;
|
||||
|
||||
records.sort((a, b) => b.date.compareTo(a.date));
|
||||
|
||||
int streak = 0;
|
||||
DateTime currentDate = DateTime.now();
|
||||
|
||||
for (final record in records) {
|
||||
final daysDiff = currentDate.difference(record.date).inDays;
|
||||
|
||||
if (daysDiff == streak) {
|
||||
streak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
/// 计算最大连续学习天数
|
||||
int _calculateMaxStreak(List<DailyStudyRecord> records) {
|
||||
if (records.isEmpty) return 0;
|
||||
|
||||
records.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
int maxStreak = 1;
|
||||
int currentStreak = 1;
|
||||
|
||||
for (int i = 1; i < records.length; i++) {
|
||||
final daysDiff = records[i].date.difference(records[i - 1].date).inDays;
|
||||
|
||||
if (daysDiff == 1) {
|
||||
currentStreak++;
|
||||
maxStreak = maxStreak > currentStreak ? maxStreak : currentStreak;
|
||||
} else {
|
||||
currentStreak = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return maxStreak;
|
||||
}
|
||||
|
||||
/// 格式化日期键
|
||||
String _formatDateKey(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
616
client/lib/features/vocabulary/services/test_data_generator.dart
Normal file
616
client/lib/features/vocabulary/services/test_data_generator.dart
Normal file
@@ -0,0 +1,616 @@
|
||||
import '../models/word_model.dart';
|
||||
import 'dart:math';
|
||||
|
||||
/// 测试数据生成器
|
||||
class TestDataGenerator {
|
||||
static final Random _random = Random();
|
||||
|
||||
/// 生成测试单词数据
|
||||
static List<Word> generateTestWords({int count = 20}) {
|
||||
final words = <Word>[];
|
||||
final testWordData = _getTestWordData();
|
||||
|
||||
for (int i = 0; i < min(count, testWordData.length); i++) {
|
||||
final data = testWordData[i];
|
||||
words.add(_createWord(data));
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/// 创建单词对象
|
||||
static Word _createWord(Map<String, dynamic> data) {
|
||||
return Word(
|
||||
id: data['id'],
|
||||
word: data['word'],
|
||||
phonetic: data['phonetic'],
|
||||
audioUrl: data['audioUrl'],
|
||||
difficulty: data['difficulty'],
|
||||
frequency: data['frequency'],
|
||||
definitions: (data['definitions'] as List).map((d) => WordDefinition(
|
||||
type: d['type'],
|
||||
definition: d['definition'],
|
||||
translation: d['translation'],
|
||||
)).toList(),
|
||||
examples: (data['examples'] as List).map((e) => WordExample(
|
||||
sentence: e['sentence'],
|
||||
translation: e['translation'],
|
||||
)).toList(),
|
||||
synonyms: List<String>.from(data['synonyms'] ?? []),
|
||||
antonyms: List<String>.from(data['antonyms'] ?? []),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取测试单词数据
|
||||
static List<Map<String, dynamic>> _getTestWordData() {
|
||||
return [
|
||||
{
|
||||
'id': 'test_word_1',
|
||||
'word': 'innovation',
|
||||
'phonetic': '/ˌɪnəˈveɪʃn/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=innovation&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 8500,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.noun,
|
||||
'definition': 'A new method, idea, product, etc.',
|
||||
'translation': '创新;革新;新方法',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The company is known for its technological innovations.',
|
||||
'translation': '这家公司以其技术创新而闻名。',
|
||||
},
|
||||
{
|
||||
'sentence': 'Innovation is the key to success in business.',
|
||||
'translation': '创新是商业成功的关键。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['novelty', 'invention', 'breakthrough'],
|
||||
'antonyms': ['tradition', 'convention'],
|
||||
'tags': ['business', 'technology', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_2',
|
||||
'word': 'persistent',
|
||||
'phonetic': '/pərˈsɪstənt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=persistent&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 7200,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Continuing firmly or obstinately in a course of action despite difficulty or opposition.',
|
||||
'translation': '坚持不懈的;执着的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'She is very persistent in pursuing her goals.',
|
||||
'translation': '她在追求目标时非常执着。',
|
||||
},
|
||||
{
|
||||
'sentence': 'His persistent efforts finally paid off.',
|
||||
'translation': '他坚持不懈的努力终于得到了回报。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['determined', 'tenacious', 'steadfast'],
|
||||
'antonyms': ['inconsistent', 'wavering'],
|
||||
'tags': ['character', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_3',
|
||||
'word': 'enthusiasm',
|
||||
'phonetic': '/ɪnˈθuziæzəm/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=enthusiasm&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6800,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.noun,
|
||||
'definition': 'Intense and eager enjoyment, interest, or approval.',
|
||||
'translation': '热情;热忱;热心',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'She showed great enthusiasm for the project.',
|
||||
'translation': '她对这个项目表现出极大的热情。',
|
||||
},
|
||||
{
|
||||
'sentence': 'His enthusiasm is contagious.',
|
||||
'translation': '他的热情很有感染力。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['passion', 'eagerness', 'zeal'],
|
||||
'antonyms': ['apathy', 'indifference'],
|
||||
'tags': ['emotion', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_4',
|
||||
'word': 'collaborate',
|
||||
'phonetic': '/kəˈlæbəreɪt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=collaborate&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 5900,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Work jointly on an activity or project.',
|
||||
'translation': '合作;协作',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The two companies collaborated on the research project.',
|
||||
'translation': '这两家公司在研究项目上进行了合作。',
|
||||
},
|
||||
{
|
||||
'sentence': 'We need to collaborate more effectively.',
|
||||
'translation': '我们需要更有效地合作。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['cooperate', 'work together', 'team up'],
|
||||
'antonyms': ['compete', 'oppose'],
|
||||
'tags': ['work', 'teamwork', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_5',
|
||||
'word': 'efficient',
|
||||
'phonetic': '/ɪˈfɪʃnt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=efficient&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 7500,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Achieving maximum productivity with minimum wasted effort or expense.',
|
||||
'translation': '高效的;效率高的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to find a more efficient way to do this.',
|
||||
'translation': '我们需要找到一个更高效的方法来做这件事。',
|
||||
},
|
||||
{
|
||||
'sentence': 'She is very efficient at her job.',
|
||||
'translation': '她工作非常高效。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['effective', 'productive', 'competent'],
|
||||
'antonyms': ['inefficient', 'wasteful'],
|
||||
'tags': ['work', 'productivity', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_6',
|
||||
'word': 'analyze',
|
||||
'phonetic': '/ˈænəlaɪz/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=analyze&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 8200,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Examine in detail the structure of something.',
|
||||
'translation': '分析;分解',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to analyze the data carefully.',
|
||||
'translation': '我们需要仔细分析这些数据。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The report analyzes the current market trends.',
|
||||
'translation': '这份报告分析了当前的市场趋势。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['examine', 'study', 'investigate'],
|
||||
'antonyms': ['synthesize', 'combine'],
|
||||
'tags': ['research', 'thinking', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_7',
|
||||
'word': 'comprehensive',
|
||||
'phonetic': '/ˌkɑːmprɪˈhensɪv/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=comprehensive&type=1',
|
||||
'difficulty': WordDifficulty.advanced,
|
||||
'frequency': 6500,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Including or dealing with all or nearly all elements or aspects of something.',
|
||||
'translation': '综合的;全面的;详尽的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The book provides a comprehensive overview of the subject.',
|
||||
'translation': '这本书提供了该主题的全面概述。',
|
||||
},
|
||||
{
|
||||
'sentence': 'We need a comprehensive solution to this problem.',
|
||||
'translation': '我们需要一个全面的解决方案来解决这个问题。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['complete', 'thorough', 'extensive'],
|
||||
'antonyms': ['partial', 'incomplete'],
|
||||
'tags': ['description', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_8',
|
||||
'word': 'demonstrate',
|
||||
'phonetic': '/ˈdemənstreɪt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=demonstrate&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 7800,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Clearly show the existence or truth of something by giving proof or evidence.',
|
||||
'translation': '证明;展示;演示',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The study demonstrates the effectiveness of the new method.',
|
||||
'translation': '这项研究证明了新方法的有效性。',
|
||||
},
|
||||
{
|
||||
'sentence': 'Let me demonstrate how to use this tool.',
|
||||
'translation': '让我演示一下如何使用这个工具。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['show', 'prove', 'illustrate'],
|
||||
'antonyms': ['conceal', 'hide'],
|
||||
'tags': ['action', 'proof', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_9',
|
||||
'word': 'significant',
|
||||
'phonetic': '/sɪɡˈnɪfɪkənt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=significant&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 9200,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Sufficiently great or important to be worthy of attention.',
|
||||
'translation': '重要的;显著的;有意义的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'There has been a significant improvement in sales.',
|
||||
'translation': '销售额有了显著的提高。',
|
||||
},
|
||||
{
|
||||
'sentence': 'This is a significant achievement.',
|
||||
'translation': '这是一个重大的成就。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['important', 'notable', 'considerable'],
|
||||
'antonyms': ['insignificant', 'trivial'],
|
||||
'tags': ['importance', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_10',
|
||||
'word': 'implement',
|
||||
'phonetic': '/ˈɪmplɪment/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=implement&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6700,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Put a decision or plan into effect.',
|
||||
'translation': '实施;执行;实现',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The company will implement the new policy next month.',
|
||||
'translation': '公司将在下个月实施新政策。',
|
||||
},
|
||||
{
|
||||
'sentence': 'We need to implement these changes immediately.',
|
||||
'translation': '我们需要立即实施这些变更。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['execute', 'carry out', 'apply'],
|
||||
'antonyms': ['abandon', 'neglect'],
|
||||
'tags': ['action', 'business', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_11',
|
||||
'word': 'strategy',
|
||||
'phonetic': '/ˈstrætədʒi/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=strategy&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 8900,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.noun,
|
||||
'definition': 'A plan of action designed to achieve a long-term or overall aim.',
|
||||
'translation': '策略;战略;计划',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to develop a new marketing strategy.',
|
||||
'translation': '我们需要制定一个新的营销策略。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The company\'s strategy is to expand into new markets.',
|
||||
'translation': '公司的战略是扩展到新市场。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['plan', 'approach', 'tactic'],
|
||||
'antonyms': [],
|
||||
'tags': ['business', 'planning', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_12',
|
||||
'word': 'perspective',
|
||||
'phonetic': '/pərˈspektɪv/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=perspective&type=1',
|
||||
'difficulty': WordDifficulty.advanced,
|
||||
'frequency': 7100,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.noun,
|
||||
'definition': 'A particular attitude toward or way of regarding something; a point of view.',
|
||||
'translation': '观点;视角;看法',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to look at this from a different perspective.',
|
||||
'translation': '我们需要从不同的角度来看待这个问题。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The book offers a fresh perspective on the issue.',
|
||||
'translation': '这本书对这个问题提供了新的视角。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['viewpoint', 'outlook', 'angle'],
|
||||
'antonyms': [],
|
||||
'tags': ['thinking', 'opinion', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_13',
|
||||
'word': 'enhance',
|
||||
'phonetic': '/ɪnˈhæns/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=enhance&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6400,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Intensify, increase, or further improve the quality, value, or extent of.',
|
||||
'translation': '提高;增强;改善',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'This will enhance the quality of our products.',
|
||||
'translation': '这将提高我们产品的质量。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The new features enhance user experience.',
|
||||
'translation': '新功能增强了用户体验。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['improve', 'boost', 'strengthen'],
|
||||
'antonyms': ['diminish', 'reduce'],
|
||||
'tags': ['improvement', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_14',
|
||||
'word': 'sustainable',
|
||||
'phonetic': '/səˈsteɪnəbl/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=sustainable&type=1',
|
||||
'difficulty': WordDifficulty.advanced,
|
||||
'frequency': 5800,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Able to be maintained at a certain rate or level.',
|
||||
'translation': '可持续的;能维持的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to develop sustainable energy sources.',
|
||||
'translation': '我们需要开发可持续的能源。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The company is committed to sustainable development.',
|
||||
'translation': '公司致力于可持续发展。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['viable', 'maintainable', 'renewable'],
|
||||
'antonyms': ['unsustainable', 'temporary'],
|
||||
'tags': ['environment', 'business', 'IELTS'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_15',
|
||||
'word': 'integrate',
|
||||
'phonetic': '/ˈɪntɪɡreɪt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=integrate&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6900,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Combine one thing with another to form a whole.',
|
||||
'translation': '整合;使结合;使一体化',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We need to integrate the new system with the existing one.',
|
||||
'translation': '我们需要将新系统与现有系统整合。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The program integrates various learning methods.',
|
||||
'translation': '该程序整合了各种学习方法。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['combine', 'merge', 'unify'],
|
||||
'antonyms': ['separate', 'divide'],
|
||||
'tags': ['combination', 'technology', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_16',
|
||||
'word': 'facilitate',
|
||||
'phonetic': '/fəˈsɪlɪteɪt/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=facilitate&type=1',
|
||||
'difficulty': WordDifficulty.advanced,
|
||||
'frequency': 5500,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Make an action or process easy or easier.',
|
||||
'translation': '促进;使便利;使容易',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'Technology can facilitate communication.',
|
||||
'translation': '技术可以促进沟通。',
|
||||
},
|
||||
{
|
||||
'sentence': 'The new software facilitates data analysis.',
|
||||
'translation': '新软件使数据分析变得更容易。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['enable', 'assist', 'help'],
|
||||
'antonyms': ['hinder', 'obstruct'],
|
||||
'tags': ['action', 'help', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_17',
|
||||
'word': 'objective',
|
||||
'phonetic': '/əbˈdʒektɪv/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=objective&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 7600,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.noun,
|
||||
'definition': 'A thing aimed at or sought; a goal.',
|
||||
'translation': '目标;目的',
|
||||
},
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Not influenced by personal feelings or opinions.',
|
||||
'translation': '客观的;不带偏见的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'Our main objective is to improve customer satisfaction.',
|
||||
'translation': '我们的主要目标是提高客户满意度。',
|
||||
},
|
||||
{
|
||||
'sentence': 'Try to be objective when making decisions.',
|
||||
'translation': '做决定时要尽量客观。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['goal', 'aim', 'target', 'impartial'],
|
||||
'antonyms': ['subjective', 'biased'],
|
||||
'tags': ['goal', 'thinking', 'CET4'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_18',
|
||||
'word': 'diverse',
|
||||
'phonetic': '/daɪˈvɜːrs/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=diverse&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6300,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Showing a great deal of variety; very different.',
|
||||
'translation': '多样的;不同的;各种各样的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'The city has a diverse population.',
|
||||
'translation': '这个城市有多样化的人口。',
|
||||
},
|
||||
{
|
||||
'sentence': 'We offer a diverse range of products.',
|
||||
'translation': '我们提供各种各样的产品。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['varied', 'different', 'assorted'],
|
||||
'antonyms': ['uniform', 'similar'],
|
||||
'tags': ['variety', 'difference', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_19',
|
||||
'word': 'fundamental',
|
||||
'phonetic': '/ˌfʌndəˈmentl/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=fundamental&type=1',
|
||||
'difficulty': WordDifficulty.advanced,
|
||||
'frequency': 7400,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.adjective,
|
||||
'definition': 'Forming a necessary base or core; of central importance.',
|
||||
'translation': '基本的;根本的;重要的',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'Education is fundamental to success.',
|
||||
'translation': '教育是成功的基础。',
|
||||
},
|
||||
{
|
||||
'sentence': 'There are fundamental differences between the two approaches.',
|
||||
'translation': '这两种方法之间存在根本性的差异。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['basic', 'essential', 'primary'],
|
||||
'antonyms': ['secondary', 'superficial'],
|
||||
'tags': ['importance', 'basic', 'CET6'],
|
||||
},
|
||||
{
|
||||
'id': 'test_word_20',
|
||||
'word': 'accomplish',
|
||||
'phonetic': '/əˈkɑːmplɪʃ/',
|
||||
'audioUrl': 'https://dict.youdao.com/dictvoice?audio=accomplish&type=1',
|
||||
'difficulty': WordDifficulty.intermediate,
|
||||
'frequency': 6100,
|
||||
'definitions': [
|
||||
{
|
||||
'type': WordType.verb,
|
||||
'definition': 'Achieve or complete successfully.',
|
||||
'translation': '完成;实现;达到',
|
||||
}
|
||||
],
|
||||
'examples': [
|
||||
{
|
||||
'sentence': 'We accomplished our goal ahead of schedule.',
|
||||
'translation': '我们提前完成了目标。',
|
||||
},
|
||||
{
|
||||
'sentence': 'She has accomplished a great deal in her career.',
|
||||
'translation': '她在职业生涯中取得了很大成就。',
|
||||
}
|
||||
],
|
||||
'synonyms': ['achieve', 'complete', 'fulfill'],
|
||||
'antonyms': ['fail', 'abandon'],
|
||||
'tags': ['achievement', 'success', 'CET4'],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import '../models/vocabulary_book_model.dart';
|
||||
import '../models/vocabulary_book_category.dart';
|
||||
|
||||
/// 词汇书数据服务
|
||||
class VocabularyDataService {
|
||||
/// 获取模拟的词汇书数据(根据分类)
|
||||
static List<VocabularyBook> getVocabularyBooksByCategory(VocabularyBookMainCategory category) {
|
||||
final baseTime = DateTime.now();
|
||||
|
||||
switch (category) {
|
||||
case VocabularyBookMainCategory.academicStage:
|
||||
return [
|
||||
_createVocabularyBook(
|
||||
id: 'primary_core_1000',
|
||||
name: '小学英语核心词汇',
|
||||
description: '小学阶段必备的1000个核心词汇,涵盖日常生活场景',
|
||||
totalWords: 1000,
|
||||
tags: ['小学', '基础', '日常用语'],
|
||||
difficulty: VocabularyBookDifficulty.beginner,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'junior_high_1500',
|
||||
name: '初中英语词汇',
|
||||
description: '初中阶段1500-2500词汇,结合教材要求',
|
||||
totalWords: 1500,
|
||||
tags: ['初中', '教材', '基础'],
|
||||
difficulty: VocabularyBookDifficulty.beginner,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'senior_high_3500',
|
||||
name: '高中英语词汇',
|
||||
description: '高中阶段2500-3500词汇,涵盖课标与高考高频词',
|
||||
totalWords: 3500,
|
||||
tags: ['高中', '高考', '课标'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'college_textbook',
|
||||
name: '大学英语教材词汇',
|
||||
description: '大学英语精读/泛读配套词汇',
|
||||
totalWords: 2000,
|
||||
tags: ['大学', '教材', '精读'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
];
|
||||
|
||||
case VocabularyBookMainCategory.domesticTest:
|
||||
return [
|
||||
_createVocabularyBook(
|
||||
id: 'cet4_vocabulary',
|
||||
name: '大学四级词汇(CET-4)',
|
||||
description: '大学英语四级考试核心词汇',
|
||||
totalWords: 4500,
|
||||
tags: ['四级', 'CET-4', '考试'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'cet6_vocabulary',
|
||||
name: '大学六级词汇(CET-6)',
|
||||
description: '大学英语六级考试核心词汇',
|
||||
totalWords: 5500,
|
||||
tags: ['六级', 'CET-6', '考试'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'postgraduate_vocabulary',
|
||||
name: '考研英语核心词汇',
|
||||
description: '考研英语必备核心词汇',
|
||||
totalWords: 5500,
|
||||
tags: ['考研', '研究生', '核心词汇'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'tem4_vocabulary',
|
||||
name: '专四词汇(TEM-4)',
|
||||
description: '英语专业四级考试词汇',
|
||||
totalWords: 8000,
|
||||
tags: ['专四', 'TEM-4', '英语专业'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'tem8_vocabulary',
|
||||
name: '专八词汇(TEM-8)',
|
||||
description: '英语专业八级考试词汇',
|
||||
totalWords: 12000,
|
||||
tags: ['专八', 'TEM-8', '英语专业'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
];
|
||||
|
||||
case VocabularyBookMainCategory.internationalTest:
|
||||
return [
|
||||
_createVocabularyBook(
|
||||
id: 'ielts_academic',
|
||||
name: '雅思学术词汇(IELTS Academic)',
|
||||
description: '雅思学术类考试核心词汇',
|
||||
totalWords: 8000,
|
||||
tags: ['雅思', 'IELTS', '学术类'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'ielts_general',
|
||||
name: '雅思通用词汇(IELTS General)',
|
||||
description: '雅思通用类考试核心词汇',
|
||||
totalWords: 6000,
|
||||
tags: ['雅思', 'IELTS', '通用类'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'toefl_ibt',
|
||||
name: '托福词汇(TOEFL iBT)',
|
||||
description: '托福网考核心词汇',
|
||||
totalWords: 10000,
|
||||
tags: ['托福', 'TOEFL', 'iBT'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'toeic_vocabulary',
|
||||
name: '托业词汇(TOEIC)',
|
||||
description: '托业考试职场应用词汇',
|
||||
totalWords: 6000,
|
||||
tags: ['托业', 'TOEIC', '职场'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'gre_vocabulary',
|
||||
name: 'GRE词汇',
|
||||
description: 'GRE学术/研究生申请词汇',
|
||||
totalWords: 15000,
|
||||
tags: ['GRE', '研究生', '学术'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'gmat_vocabulary',
|
||||
name: 'GMAT词汇',
|
||||
description: 'GMAT商科/管理类研究生词汇',
|
||||
totalWords: 8000,
|
||||
tags: ['GMAT', '商科', '管理'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'sat_vocabulary',
|
||||
name: 'SAT词汇',
|
||||
description: 'SAT美本申请词汇',
|
||||
totalWords: 5000,
|
||||
tags: ['SAT', '美本', '申请'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
];
|
||||
|
||||
case VocabularyBookMainCategory.professional:
|
||||
return [
|
||||
_createVocabularyBook(
|
||||
id: 'bec_preliminary',
|
||||
name: '商务英语初级(BEC Preliminary)',
|
||||
description: 'BEC初级商务英语词汇',
|
||||
totalWords: 3000,
|
||||
tags: ['BEC', '商务', '初级'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'bec_vantage',
|
||||
name: '商务英语中级(BEC Vantage)',
|
||||
description: 'BEC中级商务英语词汇',
|
||||
totalWords: 4000,
|
||||
tags: ['BEC', '商务', '中级'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'bec_higher',
|
||||
name: '商务英语高级(BEC Higher)',
|
||||
description: 'BEC高级商务英语词汇',
|
||||
totalWords: 5000,
|
||||
tags: ['BEC', '商务', '高级'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'mba_finance',
|
||||
name: 'MBA/金融词汇',
|
||||
description: 'MBA、金融、会计、经济学专业词汇',
|
||||
totalWords: 6000,
|
||||
tags: ['MBA', '金融', '会计'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'medical_english',
|
||||
name: '医学英语词汇',
|
||||
description: '医学专业英语词汇',
|
||||
totalWords: 8000,
|
||||
tags: ['医学', '专业', '医疗'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'legal_english',
|
||||
name: '法律英语词汇',
|
||||
description: '法律专业英语词汇',
|
||||
totalWords: 5000,
|
||||
tags: ['法律', '专业', '司法'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'it_engineering',
|
||||
name: '工程与IT英语',
|
||||
description: '计算机科学、人工智能、软件工程词汇',
|
||||
totalWords: 4000,
|
||||
tags: ['IT', '工程', '计算机'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'academic_english',
|
||||
name: '学术英语(EAP)',
|
||||
description: '学术英语写作/阅读/科研常用词汇',
|
||||
totalWords: 6000,
|
||||
tags: ['学术', 'EAP', '科研'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
];
|
||||
|
||||
case VocabularyBookMainCategory.functional:
|
||||
return [
|
||||
_createVocabularyBook(
|
||||
id: 'word_roots_affixes',
|
||||
name: '词根词缀词汇',
|
||||
description: '帮助记忆与扩展的词根词缀词汇',
|
||||
totalWords: 3000,
|
||||
tags: ['词根', '词缀', '记忆'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'synonyms_antonyms',
|
||||
name: '同义词/反义词库',
|
||||
description: '同义词、反义词、近义搭配库',
|
||||
totalWords: 2500,
|
||||
tags: ['同义词', '反义词', '搭配'],
|
||||
difficulty: VocabularyBookDifficulty.intermediate,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'daily_spoken_collocations',
|
||||
name: '日常口语搭配库',
|
||||
description: '日常口语常用搭配库',
|
||||
totalWords: 1500,
|
||||
tags: ['口语', '搭配', '日常'],
|
||||
difficulty: VocabularyBookDifficulty.beginner,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'academic_spoken_collocations',
|
||||
name: '学术口语搭配库',
|
||||
description: '学术口语常用搭配库',
|
||||
totalWords: 2000,
|
||||
tags: ['学术', '口语', '搭配'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'academic_writing_collocations',
|
||||
name: '学术写作搭配库',
|
||||
description: '学术写作常用搭配库(Collocations)',
|
||||
totalWords: 2500,
|
||||
tags: ['学术', '写作', '搭配'],
|
||||
difficulty: VocabularyBookDifficulty.advanced,
|
||||
category: category,
|
||||
),
|
||||
_createVocabularyBook(
|
||||
id: 'daily_life_english',
|
||||
name: '日常生活英语',
|
||||
description: '旅游、点餐、购物、出行、租房等日常生活英语',
|
||||
totalWords: 2000,
|
||||
tags: ['日常', '生活', '实用'],
|
||||
difficulty: VocabularyBookDifficulty.beginner,
|
||||
category: category,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建词汇书的辅助方法
|
||||
static VocabularyBook _createVocabularyBook({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required int totalWords,
|
||||
required List<String> tags,
|
||||
required VocabularyBookDifficulty difficulty,
|
||||
required VocabularyBookMainCategory category,
|
||||
}) {
|
||||
final coverImages = [
|
||||
'https://images.unsplash.com/photo-1503676260728-1c00da094a0b?w=300',
|
||||
'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=300',
|
||||
'https://images.unsplash.com/photo-1434030216411-0b793f4b4173?w=300',
|
||||
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300',
|
||||
'https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?w=300',
|
||||
];
|
||||
|
||||
return VocabularyBook(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
type: VocabularyBookType.system,
|
||||
difficulty: difficulty,
|
||||
coverImageUrl: coverImages[id.hashCode % coverImages.length],
|
||||
totalWords: totalWords,
|
||||
isPublic: true,
|
||||
tags: tags,
|
||||
mainCategory: category,
|
||||
targetLevels: _getTargetLevels(category),
|
||||
estimatedDays: (totalWords / 20).ceil(),
|
||||
dailyWordCount: 20,
|
||||
downloadCount: (totalWords * 0.3).round(),
|
||||
rating: 4.2 + (id.hashCode % 8) * 0.1,
|
||||
reviewCount: 50 + (id.hashCode % 200),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据分类获取目标等级
|
||||
static List<String> _getTargetLevels(VocabularyBookMainCategory category) {
|
||||
switch (category) {
|
||||
case VocabularyBookMainCategory.academicStage:
|
||||
return ['小学', '初中', '高中', '大学'];
|
||||
case VocabularyBookMainCategory.domesticTest:
|
||||
return ['大学', '研究生'];
|
||||
case VocabularyBookMainCategory.internationalTest:
|
||||
return ['大学', '研究生', '出国'];
|
||||
case VocabularyBookMainCategory.professional:
|
||||
return ['职场', '专业'];
|
||||
case VocabularyBookMainCategory.functional:
|
||||
return ['通用'];
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取推荐词汇书(首页显示)
|
||||
static List<VocabularyBook> getRecommendedVocabularyBooks() {
|
||||
final recommended = <VocabularyBook>[];
|
||||
for (final category in VocabularyBookMainCategory.values) {
|
||||
final categoryBooks = getVocabularyBooksByCategory(category).take(2);
|
||||
recommended.addAll(categoryBooks);
|
||||
}
|
||||
return recommended;
|
||||
}
|
||||
}
|
||||
746
client/lib/features/vocabulary/services/vocabulary_service.dart
Normal file
746
client/lib/features/vocabulary/services/vocabulary_service.dart
Normal file
@@ -0,0 +1,746 @@
|
||||
import 'dart:math';
|
||||
import '../models/word_model.dart';
|
||||
import '../models/vocabulary_book_model.dart';
|
||||
import '../models/study_session_model.dart';
|
||||
import '../models/daily_stats_model.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../../../core/services/enhanced_api_service.dart';
|
||||
import '../../../core/models/api_response.dart';
|
||||
|
||||
/// 单词学习服务
|
||||
class VocabularyService {
|
||||
final ApiClient _apiClient;
|
||||
final StorageService _storageService;
|
||||
final EnhancedApiService _enhancedApiService = EnhancedApiService();
|
||||
final Random _random = Random();
|
||||
|
||||
VocabularyService({
|
||||
required ApiClient apiClient,
|
||||
required StorageService storageService,
|
||||
}) : _apiClient = apiClient,
|
||||
_storageService = storageService;
|
||||
|
||||
// 缓存时长配置
|
||||
static const Duration _shortCacheDuration = Duration(minutes: 5);
|
||||
static const Duration _longCacheDuration = Duration(hours: 1);
|
||||
|
||||
// ==================== 词汇书相关 ====================
|
||||
|
||||
/// 获取系统词汇书列表
|
||||
Future<List<VocabularyBook>> getSystemVocabularyBooks({
|
||||
VocabularyBookDifficulty? difficulty,
|
||||
String? category,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<VocabularyBook>>(
|
||||
'/vocabulary/books/system',
|
||||
queryParameters: {
|
||||
if (difficulty != null) 'difficulty': difficulty.name,
|
||||
if (category != null) 'category': category,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
cacheDuration: Duration.zero, // 暂时禁用缓存,避免旧数据解析错误
|
||||
fromJson: (data) {
|
||||
// 处理分页响应结构: data.items
|
||||
final responseData = data['data'];
|
||||
final List<dynamic> list = responseData is Map
|
||||
? (responseData['items'] ?? [])
|
||||
: (data['data'] ?? []);
|
||||
return list.map((json) => VocabularyBook.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取系统词汇书失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取词汇书分类列表
|
||||
Future<List<Map<String, dynamic>>> getVocabularyBookCategories() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<Map<String, dynamic>>>(
|
||||
'/vocabulary/books/categories',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) {
|
||||
final List<dynamic> list = data['data'] ?? [];
|
||||
return list.map((item) => item as Map<String, dynamic>).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取词汇书分类失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户词汇书列表
|
||||
Future<List<VocabularyBook>> getUserVocabularyBooks() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<VocabularyBook>>(
|
||||
'/vocabulary/books/user',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final List<dynamic> list = data['data'] ?? [];
|
||||
return list.map((json) => VocabularyBook.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取用户词汇书失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取词汇书详情
|
||||
Future<VocabularyBook> getVocabularyBookDetail(String bookId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<VocabularyBook>(
|
||||
'/vocabulary/books/$bookId',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) => VocabularyBook.fromJson(data['data']),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取词汇书详情失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取词汇书单词列表
|
||||
Future<List<VocabularyBookWord>> getVocabularyBookWords(
|
||||
String bookId, {
|
||||
int page = 1,
|
||||
int limit = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<VocabularyBookWord>>(
|
||||
'/vocabulary/books/$bookId/words',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
cacheDuration: Duration.zero, // 暂时禁用缓存
|
||||
fromJson: (data) {
|
||||
// 处理分页响应结构: data.items
|
||||
final responseData = data['data'];
|
||||
final List<dynamic> list = responseData is Map
|
||||
? (responseData['items'] ?? [])
|
||||
: (data['data'] ?? []);
|
||||
return list.map((json) => VocabularyBookWord.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取词汇书单词失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户在某词汇书中的学习进度(后端API优先,失败时兜底为0)
|
||||
Future<UserVocabularyBookProgress> getVocabularyBookProgress(
|
||||
String bookId, {
|
||||
bool forceRefresh = false, // 是否强制刷新(跳过缓存)
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<UserVocabularyBookProgress>(
|
||||
'/vocabulary/books/$bookId/progress',
|
||||
cacheDuration: forceRefresh ? Duration.zero : _shortCacheDuration,
|
||||
fromJson: (data) => UserVocabularyBookProgress.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
// 当后端暂未提供词书进度接口时,返回一个默认的进度对象,避免前端崩溃
|
||||
final now = DateTime.now();
|
||||
return UserVocabularyBookProgress(
|
||||
id: 'progress_$bookId',
|
||||
userId: 'current_user',
|
||||
vocabularyBookId: bookId,
|
||||
learnedWords: 0,
|
||||
masteredWords: 0,
|
||||
progressPercentage: 0.0,
|
||||
streakDays: 0,
|
||||
totalStudyDays: 0,
|
||||
averageDailyWords: 0.0,
|
||||
estimatedCompletionDate: null,
|
||||
isCompleted: false,
|
||||
completedAt: null,
|
||||
startedAt: now,
|
||||
lastStudiedAt: now,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加词汇书到用户库
|
||||
Future<void> addVocabularyBookToUser(String bookId) async {
|
||||
try {
|
||||
await _apiClient.post('/vocabulary/books/$bookId/add');
|
||||
} catch (e) {
|
||||
throw Exception('添加词汇书失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建自定义词汇书
|
||||
Future<VocabularyBook> createCustomVocabularyBook({
|
||||
required String name,
|
||||
String? description,
|
||||
List<String> wordIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/books/custom',
|
||||
data: {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'wordIds': wordIds,
|
||||
},
|
||||
);
|
||||
|
||||
return VocabularyBook.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('创建词汇书失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 单词相关 ====================
|
||||
|
||||
/// 搜索单词
|
||||
Future<List<Word>> searchWords(
|
||||
String query, {
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/words/search',
|
||||
queryParameters: {
|
||||
'q': query,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
return data.map((json) => Word.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('搜索单词失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取单词详情
|
||||
Future<Word> getWordDetail(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/vocabulary/words/$wordId');
|
||||
return Word.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('获取单词详情失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户单词学习进度
|
||||
Future<UserWordProgress?> getUserWordProgress(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/vocabulary/words/$wordId/progress');
|
||||
final data = response.data['data'];
|
||||
return data != null ? UserWordProgress.fromJson(data) : null;
|
||||
} catch (e) {
|
||||
throw Exception('获取单词学习进度失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新用户单词学习进度
|
||||
Future<UserWordProgress> updateUserWordProgress({
|
||||
required String wordId,
|
||||
required LearningStatus status,
|
||||
required bool isCorrect,
|
||||
int responseTime = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'/vocabulary/words/$wordId/progress',
|
||||
data: {
|
||||
'status': status.name,
|
||||
'isCorrect': isCorrect,
|
||||
'responseTime': responseTime,
|
||||
},
|
||||
);
|
||||
|
||||
return UserWordProgress.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('更新单词学习进度失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 学习会话相关 ====================
|
||||
|
||||
/// 开始学习会话
|
||||
Future<StudySession> startStudySession({
|
||||
String? vocabularyBookId,
|
||||
required StudyMode mode,
|
||||
required int targetWordCount,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/study/sessions',
|
||||
data: {
|
||||
'vocabularyBookId': vocabularyBookId,
|
||||
'mode': mode.name,
|
||||
'targetWordCount': targetWordCount,
|
||||
},
|
||||
);
|
||||
|
||||
return StudySession.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('开始学习会话失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 结束学习会话
|
||||
Future<StudySession> endStudySession(
|
||||
String sessionId, {
|
||||
required int durationSeconds,
|
||||
required List<WordExerciseRecord> exercises,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'/vocabulary/study/sessions/$sessionId/end',
|
||||
data: {
|
||||
'durationSeconds': durationSeconds,
|
||||
'exercises': exercises.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
);
|
||||
|
||||
return StudySession.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('结束学习会话失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习会话历史
|
||||
Future<List<StudySession>> getStudySessionHistory({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/sessions',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
if (startDate != null) 'startDate': startDate.toIso8601String(),
|
||||
if (endDate != null) 'endDate': endDate.toIso8601String(),
|
||||
},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
return data.map((json) => StudySession.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('获取学习会话历史失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 学习统计相关 ====================
|
||||
|
||||
/// 获取学习统计
|
||||
Future<StudyStatistics> getStudyStatistics(DateTime date) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/statistics',
|
||||
queryParameters: {
|
||||
'date': date.toIso8601String().split('T')[0],
|
||||
},
|
||||
);
|
||||
|
||||
return StudyStatistics.fromJson(response.data['data']);
|
||||
} catch (e) {
|
||||
throw Exception('获取学习统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取每日词汇统计(wordsLearned、studyTimeMinutes)
|
||||
Future<DailyStats> getDailyVocabularyStats({required String userId}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<DailyStats>(
|
||||
'/vocabulary/daily',
|
||||
queryParameters: {
|
||||
'user_id': userId,
|
||||
},
|
||||
useCache: false,
|
||||
fromJson: (data) => DailyStats.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取每日词汇统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户词汇整体统计(total_studied、accuracy_rate、mastery_stats等)
|
||||
Future<Map<String, dynamic>> getUserVocabularyStats() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<Map<String, dynamic>>(
|
||||
'/vocabulary/stats',
|
||||
fromJson: (data) => Map<String, dynamic>.from(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return response.data!;
|
||||
} else {
|
||||
throw Exception(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('获取用户词汇整体统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习统计历史
|
||||
Future<List<StudyStatistics>> getStudyStatisticsHistory({
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/statistics/history',
|
||||
queryParameters: {
|
||||
'startDate': startDate.toIso8601String().split('T')[0],
|
||||
'endDate': endDate.toIso8601String().split('T')[0],
|
||||
},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
return data.map((json) => StudyStatistics.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('获取学习统计历史失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 智能学习算法 ====================
|
||||
|
||||
/// 获取今日需要学习的单词
|
||||
Future<List<Word>> getTodayStudyWords({
|
||||
String? vocabularyBookId,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/today',
|
||||
queryParameters: {
|
||||
if (vocabularyBookId != null) 'vocabularyBookId': vocabularyBookId,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
|
||||
print('=== getTodayStudyWords API响应 ===');
|
||||
print('原始响应: ${response.data}');
|
||||
print('data字段类型: ${response.data['data'].runtimeType}');
|
||||
|
||||
// 后端返回的结构是 {"data": {"words": []}}
|
||||
final responseData = response.data['data'];
|
||||
|
||||
List<dynamic> data;
|
||||
if (responseData is Map) {
|
||||
print('responseData是Map,尝试获取words字段');
|
||||
final words = responseData['words'];
|
||||
print('words字段类型: ${words.runtimeType}');
|
||||
data = words is List ? words : [];
|
||||
} else if (responseData is List) {
|
||||
print('responseData是List,直接使用');
|
||||
data = responseData;
|
||||
} else {
|
||||
print('responseData类型未知: ${responseData.runtimeType}');
|
||||
data = [];
|
||||
}
|
||||
|
||||
print('最终数据条数: ${data.length}');
|
||||
if (data.isNotEmpty) {
|
||||
print('第一条数据: ${data[0]}');
|
||||
}
|
||||
|
||||
return data.map((json) => Word.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
// API调用失败,返回空列表,让UI显示空状态
|
||||
print('getTodayStudyWords API调用失败: $e');
|
||||
print('堆栈跟踪: $stackTrace');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取需要复习的单词
|
||||
Future<List<Word>> getReviewWords({
|
||||
String? vocabularyBookId,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/study/review',
|
||||
queryParameters: {
|
||||
if (vocabularyBookId != null) 'vocabularyBookId': vocabularyBookId,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
|
||||
// 后端返回的结构可能是 {"data": {"words": []}} 或 {"data": []}
|
||||
final responseData = response.data['data'];
|
||||
final List<dynamic> data = responseData is Map
|
||||
? (responseData['words'] ?? [])
|
||||
: (responseData ?? []);
|
||||
|
||||
print('=== getReviewWords API响应 ===');
|
||||
print('数据条数: ${data.length}');
|
||||
return data.map((json) => Word.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
// API调用失败,返回空列表,让UI显示空状态
|
||||
print('getReviewWords API调用失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成单词练习题
|
||||
Future<Map<String, dynamic>> generateWordExercise(
|
||||
String wordId,
|
||||
ExerciseType exerciseType,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/study/exercise',
|
||||
data: {
|
||||
'wordId': wordId,
|
||||
'exerciseType': exerciseType.name,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data['data'];
|
||||
} catch (e) {
|
||||
throw Exception('生成练习题失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 本地缓存相关 ====================
|
||||
|
||||
/// 缓存词汇书到本地
|
||||
Future<void> cacheVocabularyBook(VocabularyBook book) async {
|
||||
try {
|
||||
await _storageService.setString(
|
||||
'vocabulary_book_${book.id}',
|
||||
book.toJson().toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('缓存词汇书失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从本地获取缓存的词汇书
|
||||
Future<VocabularyBook?> getCachedVocabularyBook(String bookId) async {
|
||||
try {
|
||||
final cached = await _storageService.getString('vocabulary_book_$bookId');
|
||||
if (cached != null) {
|
||||
// 这里需要实际的JSON解析逻辑
|
||||
// return VocabularyBook.fromJson(jsonDecode(cached));
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除本地缓存
|
||||
Future<void> clearCache() async {
|
||||
try {
|
||||
// 清除所有词汇相关的缓存
|
||||
await _storageService.remove('vocabulary_books');
|
||||
await _storageService.remove('study_statistics');
|
||||
} catch (e) {
|
||||
throw Exception('清除缓存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/// 计算单词熟练度
|
||||
int calculateWordProficiency(UserWordProgress progress) {
|
||||
if (progress.studyCount == 0) return 0;
|
||||
|
||||
final accuracy = progress.accuracy;
|
||||
final studyCount = progress.studyCount;
|
||||
final reviewInterval = progress.reviewInterval;
|
||||
|
||||
// 基于准确率、学习次数和复习间隔计算熟练度
|
||||
int proficiency = (accuracy * 50).round();
|
||||
proficiency += (studyCount * 5).clamp(0, 30);
|
||||
proficiency += (reviewInterval * 2).clamp(0, 20);
|
||||
|
||||
return proficiency.clamp(0, 100);
|
||||
}
|
||||
|
||||
/// 计算下次复习时间
|
||||
DateTime calculateNextReviewTime(UserWordProgress progress) {
|
||||
final now = DateTime.now();
|
||||
final accuracy = progress.accuracy;
|
||||
|
||||
// 基于准确率调整复习间隔
|
||||
int intervalDays = progress.reviewInterval;
|
||||
|
||||
if (accuracy >= 0.9) {
|
||||
intervalDays = (intervalDays * 2).clamp(1, 30);
|
||||
} else if (accuracy >= 0.7) {
|
||||
intervalDays = (intervalDays * 1.5).round().clamp(1, 14);
|
||||
} else if (accuracy >= 0.5) {
|
||||
intervalDays = intervalDays.clamp(1, 7);
|
||||
} else {
|
||||
intervalDays = 1;
|
||||
}
|
||||
|
||||
return now.add(Duration(days: intervalDays));
|
||||
}
|
||||
|
||||
/// 随机选择练习类型
|
||||
ExerciseType getRandomExerciseType() {
|
||||
final types = ExerciseType.values;
|
||||
return types[_random.nextInt(types.length)];
|
||||
}
|
||||
|
||||
/// 生成干扰选项
|
||||
List<String> generateDistractors(
|
||||
String correctAnswer,
|
||||
List<String> wordPool,
|
||||
int count,
|
||||
) {
|
||||
final distractors = <String>[];
|
||||
final shuffled = List<String>.from(wordPool)..shuffle(_random);
|
||||
|
||||
for (final word in shuffled) {
|
||||
if (word != correctAnswer && !distractors.contains(word)) {
|
||||
distractors.add(word);
|
||||
if (distractors.length >= count) break;
|
||||
}
|
||||
}
|
||||
|
||||
return distractors;
|
||||
}
|
||||
|
||||
// ==================== 本地兜底(无后端时) ====================
|
||||
|
||||
/// 当后端不可用或接口缺失时,生成基础示例单词(仅用于开发调试)
|
||||
List<Word> _generateSampleWords(int limit) {
|
||||
final now = DateTime.now();
|
||||
final samples = [
|
||||
{'w': 'apple', 'cn': '苹果', 'def': 'a round fruit of a tree', 'type': 'noun'},
|
||||
{'w': 'run', 'cn': '跑步', 'def': 'move fast by using one’s feet', 'type': 'verb'},
|
||||
{'w': 'happy', 'cn': '开心的', 'def': 'feeling or showing pleasure', 'type': 'adjective'},
|
||||
{'w': 'quickly', 'cn': '快速地', 'def': 'at a fast speed', 'type': 'adverb'},
|
||||
{'w': 'book', 'cn': '书', 'def': 'a written or printed work', 'type': 'noun'},
|
||||
{'w': 'study', 'cn': '学习', 'def': 'apply oneself to learning', 'type': 'verb'},
|
||||
{'w': 'blue', 'cn': '蓝色的', 'def': 'of the color blue', 'type': 'adjective'},
|
||||
{'w': 'slowly', 'cn': '缓慢地', 'def': 'at a slow speed', 'type': 'adverb'},
|
||||
{'w': 'friend', 'cn': '朋友', 'def': 'a person you know well', 'type': 'noun'},
|
||||
{'w': 'listen', 'cn': '听', 'def': 'give attention to sound', 'type': 'verb'},
|
||||
{'w': 'green', 'cn': '绿色的', 'def': 'of the color green', 'type': 'adjective'},
|
||||
{'w': 'often', 'cn': '经常', 'def': 'frequently; many times', 'type': 'adverb'},
|
||||
{'w': 'school', 'cn': '学校', 'def': 'a place for learning', 'type': 'noun'},
|
||||
{'w': 'write', 'cn': '写作', 'def': 'mark letters or words on a surface', 'type': 'verb'},
|
||||
{'w': 'smart', 'cn': '聪明的', 'def': 'intelligent or clever', 'type': 'adjective'},
|
||||
{'w': 'carefully', 'cn': '小心地', 'def': 'with care or attention', 'type': 'adverb'},
|
||||
{'w': 'music', 'cn': '音乐', 'def': 'art of arranging sounds', 'type': 'noun'},
|
||||
{'w': 'learn', 'cn': '学习', 'def': 'gain knowledge or skill', 'type': 'verb'},
|
||||
{'w': 'bright', 'cn': '明亮的/聪明的', 'def': 'giving much light; intelligent', 'type': 'adjective'},
|
||||
{'w': 'rarely', 'cn': '很少', 'def': 'not often; seldom', 'type': 'adverb'},
|
||||
];
|
||||
|
||||
WordType _parseType(String t) {
|
||||
switch (t) {
|
||||
case 'noun':
|
||||
return WordType.noun;
|
||||
case 'verb':
|
||||
return WordType.verb;
|
||||
case 'adjective':
|
||||
return WordType.adjective;
|
||||
case 'adverb':
|
||||
return WordType.adverb;
|
||||
default:
|
||||
return WordType.noun;
|
||||
}
|
||||
}
|
||||
|
||||
final list = <Word>[];
|
||||
for (var i = 0; i < samples.length && list.length < limit; i++) {
|
||||
final s = samples[i];
|
||||
list.add(
|
||||
Word(
|
||||
id: 'sample_${s['w']}',
|
||||
word: s['w']!,
|
||||
definitions: [
|
||||
WordDefinition(
|
||||
type: _parseType(s['type']!),
|
||||
definition: s['def']!,
|
||||
translation: s['cn']!,
|
||||
frequency: 3,
|
||||
),
|
||||
],
|
||||
examples: [
|
||||
WordExample(
|
||||
sentence: 'I like ${s['w']}.',
|
||||
translation: '我喜欢${s['cn']}.',
|
||||
),
|
||||
],
|
||||
synonyms: const [],
|
||||
antonyms: const [],
|
||||
etymology: null,
|
||||
difficulty: WordDifficulty.beginner,
|
||||
frequency: 5,
|
||||
imageUrl: null,
|
||||
memoryTip: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// 根据ID获取单词详情
|
||||
Future<Word> getWordById(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/vocabulary/$wordId');
|
||||
final data = response.data['data'];
|
||||
return Word.fromJson(data);
|
||||
} catch (e) {
|
||||
print('获取单词详情失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
314
client/lib/features/vocabulary/services/word_book_service.dart
Normal file
314
client/lib/features/vocabulary/services/word_book_service.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'dart:convert';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../models/word_book_model.dart';
|
||||
import '../models/word_model.dart';
|
||||
|
||||
/// 生词本服务
|
||||
class WordBookService {
|
||||
final ApiClient _apiClient;
|
||||
final StorageService _storageService;
|
||||
|
||||
static const String _wordBooksKey = 'word_books';
|
||||
static const String _wordBookEntriesKey = 'word_book_entries';
|
||||
|
||||
WordBookService({
|
||||
required ApiClient apiClient,
|
||||
required StorageService storageService,
|
||||
}) : _apiClient = apiClient,
|
||||
_storageService = storageService;
|
||||
|
||||
/// 获取用户的所有生词本
|
||||
Future<List<WordBook>> getUserWordBooks() async {
|
||||
try {
|
||||
// 尝试从本地存储获取
|
||||
final localData = await _storageService.getString(_wordBooksKey);
|
||||
if (localData != null) {
|
||||
final List<dynamic> jsonList = json.decode(localData);
|
||||
return jsonList.map((json) => WordBook.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
// 如果本地没有数据,创建默认生词本
|
||||
final defaultWordBook = WordBook(
|
||||
id: 'default_word_book',
|
||||
name: '我的生词本',
|
||||
description: '收藏的生词',
|
||||
userId: 'current_user',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _saveWordBooksToLocal([defaultWordBook]);
|
||||
return [defaultWordBook];
|
||||
} catch (e) {
|
||||
throw Exception('获取生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建新的生词本
|
||||
Future<WordBook> createWordBook({
|
||||
required String name,
|
||||
String? description,
|
||||
WordBookType type = WordBookType.personal,
|
||||
}) async {
|
||||
try {
|
||||
final wordBook = WordBook(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
name: name,
|
||||
description: description,
|
||||
userId: 'current_user',
|
||||
type: type,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final existingBooks = await getUserWordBooks();
|
||||
existingBooks.add(wordBook);
|
||||
await _saveWordBooksToLocal(existingBooks);
|
||||
|
||||
return wordBook;
|
||||
} catch (e) {
|
||||
throw Exception('创建生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除生词本
|
||||
Future<void> deleteWordBook(String wordBookId) async {
|
||||
try {
|
||||
final existingBooks = await getUserWordBooks();
|
||||
existingBooks.removeWhere((book) => book.id == wordBookId);
|
||||
await _saveWordBooksToLocal(existingBooks);
|
||||
|
||||
// 同时删除该生词本的所有条目
|
||||
final entries = await getWordBookEntries(wordBookId);
|
||||
for (final entry in entries) {
|
||||
await removeWordFromBook(wordBookId, entry.wordId);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('删除生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新生词本信息
|
||||
Future<WordBook> updateWordBook(WordBook wordBook) async {
|
||||
try {
|
||||
final existingBooks = await getUserWordBooks();
|
||||
final index = existingBooks.indexWhere((book) => book.id == wordBook.id);
|
||||
|
||||
if (index == -1) {
|
||||
throw Exception('生词本不存在');
|
||||
}
|
||||
|
||||
final updatedWordBook = wordBook.copyWith(updatedAt: DateTime.now());
|
||||
existingBooks[index] = updatedWordBook;
|
||||
await _saveWordBooksToLocal(existingBooks);
|
||||
|
||||
return updatedWordBook;
|
||||
} catch (e) {
|
||||
throw Exception('更新生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加单词到生词本
|
||||
Future<WordBookEntry> addWordToBook({
|
||||
required String wordBookId,
|
||||
required Word word,
|
||||
String? note,
|
||||
List<String> tags = const [],
|
||||
}) async {
|
||||
try {
|
||||
// 检查单词是否已存在
|
||||
final existingEntries = await getWordBookEntries(wordBookId);
|
||||
final existingEntry = existingEntries.where((entry) => entry.wordId == word.id).firstOrNull;
|
||||
|
||||
if (existingEntry != null) {
|
||||
throw Exception('单词已存在于生词本中');
|
||||
}
|
||||
|
||||
final entry = WordBookEntry(
|
||||
id: '${wordBookId}_${word.id}',
|
||||
wordBookId: wordBookId,
|
||||
wordId: word.id,
|
||||
word: word,
|
||||
note: note,
|
||||
tags: tags,
|
||||
addedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
existingEntries.add(entry);
|
||||
await _saveWordBookEntriesToLocal(existingEntries);
|
||||
|
||||
// 更新生词本的单词数量
|
||||
await _updateWordBookCount(wordBookId);
|
||||
|
||||
return entry;
|
||||
} catch (e) {
|
||||
throw Exception('添加单词到生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从生词本移除单词
|
||||
Future<void> removeWordFromBook(String wordBookId, String wordId) async {
|
||||
try {
|
||||
final existingEntries = await getWordBookEntries(wordBookId);
|
||||
existingEntries.removeWhere((entry) =>
|
||||
entry.wordBookId == wordBookId && entry.wordId == wordId);
|
||||
await _saveWordBookEntriesToLocal(existingEntries);
|
||||
|
||||
// 更新生词本的单词数量
|
||||
await _updateWordBookCount(wordBookId);
|
||||
} catch (e) {
|
||||
throw Exception('从生词本移除单词失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取生词本的所有条目
|
||||
Future<List<WordBookEntry>> getWordBookEntries(String wordBookId) async {
|
||||
try {
|
||||
final localData = await _storageService.getString(_wordBookEntriesKey);
|
||||
if (localData == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> jsonList = json.decode(localData);
|
||||
final allEntries = jsonList.map((json) => WordBookEntry.fromJson(json)).toList();
|
||||
|
||||
return allEntries.where((entry) => entry.wordBookId == wordBookId).toList();
|
||||
} catch (e) {
|
||||
throw Exception('获取生词本条目失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新生词本条目
|
||||
Future<WordBookEntry> updateWordBookEntry(WordBookEntry entry) async {
|
||||
try {
|
||||
final allEntries = await _getAllWordBookEntries();
|
||||
final index = allEntries.indexWhere((e) => e.id == entry.id);
|
||||
|
||||
if (index == -1) {
|
||||
throw Exception('生词本条目不存在');
|
||||
}
|
||||
|
||||
allEntries[index] = entry;
|
||||
await _saveWordBookEntriesToLocal(allEntries);
|
||||
|
||||
return entry;
|
||||
} catch (e) {
|
||||
throw Exception('更新生词本条目失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取生词本统计信息
|
||||
Future<WordBookStats> getWordBookStats(String wordBookId) async {
|
||||
try {
|
||||
final entries = await getWordBookEntries(wordBookId);
|
||||
|
||||
final totalWords = entries.length;
|
||||
final masteredWords = entries.where((entry) =>
|
||||
entry.reviewCount >= 3 &&
|
||||
entry.correctCount / entry.reviewCount >= 0.8
|
||||
).length;
|
||||
final reviewingWords = entries.where((entry) =>
|
||||
entry.reviewCount > 0 && entry.reviewCount < 3
|
||||
).length;
|
||||
final newWords = entries.where((entry) => entry.reviewCount == 0).length;
|
||||
|
||||
final masteryRate = totalWords > 0 ? masteredWords / totalWords : 0.0;
|
||||
|
||||
final lastStudyAt = entries
|
||||
.where((entry) => entry.lastReviewAt != null)
|
||||
.map((entry) => entry.lastReviewAt!)
|
||||
.fold<DateTime?>(null, (latest, current) =>
|
||||
latest == null || current.isAfter(latest) ? current : latest);
|
||||
|
||||
return WordBookStats(
|
||||
wordBookId: wordBookId,
|
||||
totalWords: totalWords,
|
||||
masteredWords: masteredWords,
|
||||
reviewingWords: reviewingWords,
|
||||
newWords: newWords,
|
||||
masteryRate: masteryRate,
|
||||
lastStudyAt: lastStudyAt,
|
||||
totalReviews: entries.fold(0, (sum, entry) => sum + entry.reviewCount),
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('获取生词本统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查单词是否在生词本中
|
||||
Future<bool> isWordInBook(String wordBookId, String wordId) async {
|
||||
try {
|
||||
final entries = await getWordBookEntries(wordBookId);
|
||||
return entries.any((entry) => entry.wordId == wordId);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 搜索生词本中的单词
|
||||
Future<List<WordBookEntry>> searchWordsInBook({
|
||||
required String wordBookId,
|
||||
required String query,
|
||||
}) async {
|
||||
try {
|
||||
final entries = await getWordBookEntries(wordBookId);
|
||||
final lowerQuery = query.toLowerCase();
|
||||
|
||||
return entries.where((entry) {
|
||||
final word = entry.word.word.toLowerCase();
|
||||
final definitions = entry.word.definitions
|
||||
.map((def) => def.definition.toLowerCase())
|
||||
.join(' ');
|
||||
final translations = entry.word.definitions
|
||||
.map((def) => def.translation?.toLowerCase() ?? '')
|
||||
.join(' ');
|
||||
|
||||
return word.contains(lowerQuery) ||
|
||||
definitions.contains(lowerQuery) ||
|
||||
translations.contains(lowerQuery);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
throw Exception('搜索生词本失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存生词本到本地存储
|
||||
Future<void> _saveWordBooksToLocal(List<WordBook> wordBooks) async {
|
||||
final jsonList = wordBooks.map((book) => book.toJson()).toList();
|
||||
await _storageService.setString(_wordBooksKey, json.encode(jsonList));
|
||||
}
|
||||
|
||||
/// 保存生词本条目到本地存储
|
||||
Future<void> _saveWordBookEntriesToLocal(List<WordBookEntry> entries) async {
|
||||
final jsonList = entries.map((entry) => entry.toJson()).toList();
|
||||
await _storageService.setString(_wordBookEntriesKey, json.encode(jsonList));
|
||||
}
|
||||
|
||||
/// 获取所有生词本条目
|
||||
Future<List<WordBookEntry>> _getAllWordBookEntries() async {
|
||||
final localData = await _storageService.getString(_wordBookEntriesKey);
|
||||
if (localData == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> jsonList = json.decode(localData);
|
||||
return jsonList.map((json) => WordBookEntry.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
/// 更新生词本的单词数量
|
||||
Future<void> _updateWordBookCount(String wordBookId) async {
|
||||
final entries = await getWordBookEntries(wordBookId);
|
||||
final wordBooks = await getUserWordBooks();
|
||||
|
||||
final index = wordBooks.indexWhere((book) => book.id == wordBookId);
|
||||
if (index != -1) {
|
||||
final updatedBook = wordBooks[index].copyWith(
|
||||
wordCount: entries.length,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
wordBooks[index] = updatedBook;
|
||||
await _saveWordBooksToLocal(wordBooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user