init
This commit is contained in:
332
client/lib/shared/services/audio_service.dart
Normal file
332
client/lib/shared/services/audio_service.dart
Normal file
@@ -0,0 +1,332 @@
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/errors/app_error.dart';
|
||||
import '../models/api_response.dart';
|
||||
|
||||
/// 音频播放状态
|
||||
enum AudioPlayerState {
|
||||
stopped,
|
||||
playing,
|
||||
paused,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
|
||||
/// 音频服务
|
||||
class AudioService {
|
||||
static final AudioService _instance = AudioService._internal();
|
||||
factory AudioService() => _instance;
|
||||
AudioService._internal();
|
||||
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
AudioPlayerState _state = AudioPlayerState.stopped;
|
||||
Duration _duration = Duration.zero;
|
||||
Duration _position = Duration.zero;
|
||||
double _volume = 1.0;
|
||||
double _playbackRate = 1.0;
|
||||
String? _currentUrl;
|
||||
|
||||
// 回调函数
|
||||
Function(AudioPlayerState)? onStateChanged;
|
||||
Function(Duration)? onDurationChanged;
|
||||
Function(Duration)? onPositionChanged;
|
||||
Function(String)? onError;
|
||||
|
||||
/// 更新播放状态
|
||||
void _updateState(AudioPlayerState state) {
|
||||
_state = state;
|
||||
onStateChanged?.call(state);
|
||||
}
|
||||
|
||||
/// 播放网络音频
|
||||
Future<void> playFromUrl(String url) async {
|
||||
try {
|
||||
_updateState(AudioPlayerState.loading);
|
||||
_currentUrl = url;
|
||||
|
||||
// TODO: 实现音频播放逻辑
|
||||
_updateState(AudioPlayerState.playing);
|
||||
} catch (e) {
|
||||
_updateState(AudioPlayerState.error);
|
||||
onError?.call('播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放本地音频文件
|
||||
Future<void> playFromFile(String filePath) async {
|
||||
try {
|
||||
_updateState(AudioPlayerState.loading);
|
||||
_currentUrl = filePath;
|
||||
|
||||
// TODO: 实现本地音频播放逻辑
|
||||
_updateState(AudioPlayerState.playing);
|
||||
} catch (e) {
|
||||
_updateState(AudioPlayerState.error);
|
||||
onError?.call('播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放资源文件
|
||||
Future<void> playFromAsset(String assetPath) async {
|
||||
try {
|
||||
_updateState(AudioPlayerState.loading);
|
||||
_currentUrl = assetPath;
|
||||
|
||||
// TODO: 实现资源音频播放逻辑
|
||||
_updateState(AudioPlayerState.playing);
|
||||
} catch (e) {
|
||||
_updateState(AudioPlayerState.error);
|
||||
onError?.call('播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停播放
|
||||
Future<void> pause() async {
|
||||
try {
|
||||
// TODO: 实现暂停逻辑
|
||||
_updateState(AudioPlayerState.paused);
|
||||
} catch (e) {
|
||||
onError?.call('暂停失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复播放
|
||||
Future<void> resume() async {
|
||||
try {
|
||||
// TODO: 实现恢复播放逻辑
|
||||
_updateState(AudioPlayerState.playing);
|
||||
} catch (e) {
|
||||
onError?.call('恢复播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止播放
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
// TODO: 实现停止播放逻辑
|
||||
_updateState(AudioPlayerState.stopped);
|
||||
_position = Duration.zero;
|
||||
} catch (e) {
|
||||
onError?.call('停止播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到指定位置
|
||||
Future<void> seek(Duration position) async {
|
||||
try {
|
||||
// TODO: 实现跳转逻辑
|
||||
_position = position;
|
||||
} catch (e) {
|
||||
onError?.call('跳转失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置音量 (0.0 - 1.0)
|
||||
Future<void> setVolume(double volume) async {
|
||||
try {
|
||||
_volume = volume.clamp(0.0, 1.0);
|
||||
// TODO: 实现音量设置逻辑
|
||||
} catch (e) {
|
||||
onError?.call('设置音量失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置播放速度 (0.5 - 2.0)
|
||||
Future<void> setPlaybackRate(double rate) async {
|
||||
try {
|
||||
_playbackRate = rate.clamp(0.5, 2.0);
|
||||
// TODO: 实现播放速度设置逻辑
|
||||
} catch (e) {
|
||||
onError?.call('设置播放速度失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载音频文件
|
||||
Future<ApiResponse<String>> downloadAudio({
|
||||
required String url,
|
||||
required String fileName,
|
||||
Function(int, int)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final audioDir = Directory('${directory.path}/audio');
|
||||
|
||||
if (!await audioDir.exists()) {
|
||||
await audioDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final filePath = '${audioDir.path}/$fileName';
|
||||
|
||||
// TODO: 实现文件下载逻辑
|
||||
final response = await _apiClient.get(url);
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(response.data);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: '音频下载成功',
|
||||
data: filePath,
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '音频下载失败: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传音频文件
|
||||
Future<ApiResponse<Map<String, dynamic>>> uploadAudio({
|
||||
required String filePath,
|
||||
required String type, // 'pronunciation', 'speaking', etc.
|
||||
Map<String, dynamic>? metadata,
|
||||
Function(int, int)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
return ApiResponse.failure(
|
||||
message: '音频文件不存在',
|
||||
error: 'FILE_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'audio': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: file.path.split('/').last,
|
||||
),
|
||||
'type': type,
|
||||
if (metadata != null) 'metadata': metadata,
|
||||
});
|
||||
|
||||
final response = await _apiClient.post(
|
||||
'/audio/upload',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '音频上传成功',
|
||||
data: response.data['data'],
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? '音频上传失败',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '音频上传失败: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取音频文件信息
|
||||
Future<ApiResponse<Map<String, dynamic>>> getAudioInfo(String audioId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/audio/$audioId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: 'Audio info retrieved successfully',
|
||||
data: response.data['data'],
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get audio info',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get audio info: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除本地音频文件
|
||||
Future<bool> deleteLocalAudio(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
onError?.call('删除音频文件失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理缓存的音频文件
|
||||
Future<void> clearAudioCache() async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final audioDir = Directory('${directory.path}/audio');
|
||||
|
||||
if (await audioDir.exists()) {
|
||||
await audioDir.delete(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
onError?.call('清理音频缓存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查音频文件是否存在
|
||||
Future<bool> isAudioCached(String fileName) async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final filePath = '${directory.path}/audio/$fileName';
|
||||
final file = File(filePath);
|
||||
return await file.exists();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取缓存音频文件路径
|
||||
Future<String?> getCachedAudioPath(String fileName) async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final filePath = '${directory.path}/audio/$fileName';
|
||||
final file = File(filePath);
|
||||
|
||||
if (await file.exists()) {
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
Future<void> dispose() async {
|
||||
// TODO: 实现资源释放逻辑
|
||||
}
|
||||
|
||||
// Getters
|
||||
AudioPlayerState get state => _state;
|
||||
Duration get duration => _duration;
|
||||
Duration get position => _position;
|
||||
double get volume => _volume;
|
||||
double get playbackRate => _playbackRate;
|
||||
String? get currentUrl => _currentUrl;
|
||||
bool get isPlaying => _state == AudioPlayerState.playing;
|
||||
bool get isPaused => _state == AudioPlayerState.paused;
|
||||
bool get isStopped => _state == AudioPlayerState.stopped;
|
||||
bool get isLoading => _state == AudioPlayerState.loading;
|
||||
|
||||
/// 获取播放进度百分比 (0.0 - 1.0)
|
||||
double get progress {
|
||||
if (_duration.inMilliseconds == 0) return 0.0;
|
||||
return _position.inMilliseconds / _duration.inMilliseconds;
|
||||
}
|
||||
}
|
||||
371
client/lib/shared/services/auth_service.dart
Normal file
371
client/lib/shared/services/auth_service.dart
Normal file
@@ -0,0 +1,371 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/storage/storage_service.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/errors/app_error.dart';
|
||||
import '../models/api_response.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// 认证服务
|
||||
class AuthService {
|
||||
static final AuthService _instance = AuthService._internal();
|
||||
factory AuthService() => _instance;
|
||||
AuthService._internal();
|
||||
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
/// 用户注册
|
||||
Future<ApiResponse<AuthResponse>> register({
|
||||
required String username,
|
||||
required String email,
|
||||
required String password,
|
||||
required String nickname,
|
||||
String? phone,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/auth/register',
|
||||
data: {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'nickname': nickname,
|
||||
if (phone != null) 'phone': phone,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final authResponse = AuthResponse.fromJson(response.data['data']);
|
||||
await _saveTokens(authResponse);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '注册成功',
|
||||
data: authResponse,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? '注册失败',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '注册失败:$e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户登录
|
||||
Future<ApiResponse<AuthResponse>> login({
|
||||
required String account, // 用户名或邮箱
|
||||
required String password,
|
||||
bool rememberMe = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/auth/login',
|
||||
data: {
|
||||
'account': account,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final authResponse = AuthResponse.fromJson(response.data['data']);
|
||||
await _saveTokens(authResponse);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '登录成功',
|
||||
data: authResponse,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? '登录失败',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '登录失败:$e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新Token
|
||||
Future<ApiResponse<AuthResponse>> refreshToken() async {
|
||||
try {
|
||||
final refreshToken = StorageService.getString(AppConstants.refreshTokenKey);
|
||||
if (refreshToken == null) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Refresh token not found',
|
||||
code: 401,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _apiClient.post(
|
||||
'/auth/refresh',
|
||||
data: {
|
||||
'refresh_token': refreshToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final authResponse = AuthResponse.fromJson(response.data['data']);
|
||||
await _saveTokens(authResponse);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'Token refreshed successfully',
|
||||
data: authResponse,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Token refresh failed',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Token refresh failed: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户登出
|
||||
Future<ApiResponse<void>> logout() async {
|
||||
try {
|
||||
final response = await _apiClient.post('/auth/logout');
|
||||
|
||||
// 无论服务器响应如何,都清除本地token
|
||||
await _clearTokens();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '登出成功',
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.success(
|
||||
message: '登出成功',
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// 即使请求失败,也清除本地token
|
||||
await _clearTokens();
|
||||
return ApiResponse.success(
|
||||
message: '登出成功',
|
||||
);
|
||||
} catch (e) {
|
||||
await _clearTokens();
|
||||
return ApiResponse.success(
|
||||
message: '登出成功',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前用户信息
|
||||
Future<ApiResponse<UserModel>> getCurrentUser() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/auth/me');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final user = UserModel.fromJson(response.data['data']);
|
||||
await StorageService.setObject(AppConstants.userInfoKey, user.toJson());
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'User info retrieved successfully',
|
||||
data: user,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get user info',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get user info: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新用户信息
|
||||
Future<ApiResponse<UserModel>> updateProfile({
|
||||
String? nickname,
|
||||
String? avatar,
|
||||
String? phone,
|
||||
DateTime? birthday,
|
||||
String? gender,
|
||||
String? bio,
|
||||
String? learningLevel,
|
||||
String? targetLanguage,
|
||||
String? nativeLanguage,
|
||||
int? dailyGoal,
|
||||
}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{};
|
||||
if (nickname != null) data['nickname'] = nickname;
|
||||
if (avatar != null) data['avatar'] = avatar;
|
||||
if (phone != null) data['phone'] = phone;
|
||||
if (birthday != null) data['birthday'] = birthday.toIso8601String();
|
||||
if (gender != null) data['gender'] = gender;
|
||||
if (bio != null) data['bio'] = bio;
|
||||
if (learningLevel != null) data['learning_level'] = learningLevel;
|
||||
if (targetLanguage != null) data['target_language'] = targetLanguage;
|
||||
if (nativeLanguage != null) data['native_language'] = nativeLanguage;
|
||||
if (dailyGoal != null) data['daily_goal'] = dailyGoal;
|
||||
|
||||
final response = await _apiClient.put('/auth/profile', data: data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final user = UserModel.fromJson(response.data['data']);
|
||||
await StorageService.setObject(AppConstants.userInfoKey, user.toJson());
|
||||
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '个人信息更新成功',
|
||||
data: user,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? '个人信息更新失败',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '个人信息更新失败:$e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改密码
|
||||
Future<ApiResponse<void>> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
required String confirmPassword,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'/auth/change-password',
|
||||
data: {
|
||||
'current_password': currentPassword,
|
||||
'new_password': newPassword,
|
||||
'confirm_password': confirmPassword,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? '密码修改成功',
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? '密码修改失败',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: '密码修改失败:$e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否已登录
|
||||
bool isLoggedIn() {
|
||||
final token = StorageService.getString(AppConstants.accessTokenKey);
|
||||
return token != null && token.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 获取本地存储的用户信息
|
||||
UserModel? getCachedUser() {
|
||||
final userJson = StorageService.getObject(AppConstants.userInfoKey);
|
||||
if (userJson != null) {
|
||||
try {
|
||||
return UserModel.fromJson(userJson);
|
||||
} catch (e) {
|
||||
print('Error parsing cached user: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 保存tokens
|
||||
Future<void> _saveTokens(AuthResponse authResponse) async {
|
||||
await StorageService.setString(
|
||||
AppConstants.accessTokenKey,
|
||||
authResponse.accessToken,
|
||||
);
|
||||
await StorageService.setString(
|
||||
AppConstants.refreshTokenKey,
|
||||
authResponse.refreshToken,
|
||||
);
|
||||
|
||||
if (authResponse.user != null) {
|
||||
await StorageService.setObject(
|
||||
AppConstants.userInfoKey,
|
||||
authResponse.user!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除tokens
|
||||
Future<void> _clearTokens() async {
|
||||
await StorageService.remove(AppConstants.accessTokenKey);
|
||||
await StorageService.remove(AppConstants.refreshTokenKey);
|
||||
await StorageService.remove(AppConstants.userInfoKey);
|
||||
}
|
||||
|
||||
/// 处理Dio错误
|
||||
ApiResponse<T> _handleDioError<T>(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return ApiResponse.failure(
|
||||
message: '请求超时,请检查网络连接',
|
||||
error: 'TIMEOUT',
|
||||
);
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final message = e.response?.data?['message'] ?? '请求失败';
|
||||
return ApiResponse.failure(
|
||||
message: message,
|
||||
code: statusCode,
|
||||
error: 'BAD_RESPONSE',
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return ApiResponse.failure(
|
||||
message: '请求已取消',
|
||||
error: 'CANCELLED',
|
||||
);
|
||||
case DioExceptionType.connectionError:
|
||||
return ApiResponse.failure(
|
||||
message: '网络连接失败,请检查网络设置',
|
||||
error: 'CONNECTION_ERROR',
|
||||
);
|
||||
default:
|
||||
return ApiResponse.failure(
|
||||
message: '未知错误:${e.message}',
|
||||
error: 'UNKNOWN',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
109
client/lib/shared/services/notification_service.dart
Normal file
109
client/lib/shared/services/notification_service.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import '../../../core/models/api_response.dart';
|
||||
import '../../../core/services/enhanced_api_service.dart';
|
||||
import '../models/notification_model.dart';
|
||||
|
||||
/// 通知服务
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final EnhancedApiService _enhancedApiService = EnhancedApiService();
|
||||
|
||||
// 缓存时长配置
|
||||
static const Duration _shortCacheDuration = Duration(seconds: 30);
|
||||
|
||||
/// 获取通知列表
|
||||
Future<ApiResponse<Map<String, dynamic>>> getNotifications({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool onlyUnread = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<Map<String, dynamic>>(
|
||||
'/notifications',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'only_unread': onlyUnread,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final notifications = (data['notifications'] as List?)
|
||||
?.map((json) => NotificationModel.fromJson(json))
|
||||
.toList() ??
|
||||
[];
|
||||
return {
|
||||
'notifications': notifications,
|
||||
'total': data['total'] ?? 0,
|
||||
'page': data['page'] ?? 1,
|
||||
'limit': data['limit'] ?? 10,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('获取通知列表异常: $e');
|
||||
return ApiResponse.error(message: '获取通知列表失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取未读通知数量
|
||||
Future<ApiResponse<int>> getUnreadCount() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<int>(
|
||||
'/notifications/unread-count',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) => data['count'] ?? 0,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('获取未读通知数量异常: $e');
|
||||
return ApiResponse.error(message: '获取未读通知数量失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记通知为已读
|
||||
Future<ApiResponse<void>> markAsRead(int notificationId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<void>(
|
||||
'/notifications/$notificationId/read',
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('标记通知已读异常: $e');
|
||||
return ApiResponse.error(message: '标记通知已读失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记所有通知为已读
|
||||
Future<ApiResponse<void>> markAllAsRead() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<void>(
|
||||
'/notifications/read-all',
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('标记所有通知已读异常: $e');
|
||||
return ApiResponse.error(message: '标记所有通知已读失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除通知
|
||||
Future<ApiResponse<void>> deleteNotification(int notificationId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.delete<void>(
|
||||
'/notifications/$notificationId',
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('删除通知异常: $e');
|
||||
return ApiResponse.error(message: '删除通知失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
106
client/lib/shared/services/study_plan_service.dart
Normal file
106
client/lib/shared/services/study_plan_service.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../shared/services/api_service.dart';
|
||||
|
||||
/// 学习计划服务
|
||||
class StudyPlanService {
|
||||
final ApiService _apiService;
|
||||
|
||||
StudyPlanService(this._apiService);
|
||||
|
||||
/// 创建学习计划
|
||||
Future<Map<String, dynamic>> createStudyPlan({
|
||||
required String planName,
|
||||
String? description,
|
||||
required int dailyGoal,
|
||||
String? bookId,
|
||||
required String startDate,
|
||||
String? endDate,
|
||||
String? remindTime,
|
||||
String? remindDays,
|
||||
}) async {
|
||||
final response = await _apiService.post(
|
||||
'/study-plans',
|
||||
data: {
|
||||
'plan_name': planName,
|
||||
'description': description,
|
||||
'daily_goal': dailyGoal,
|
||||
'book_id': bookId,
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
'remind_time': remindTime,
|
||||
'remind_days': remindDays,
|
||||
},
|
||||
);
|
||||
return response['data'];
|
||||
}
|
||||
|
||||
/// 获取学习计划列表
|
||||
Future<List<dynamic>> getUserStudyPlans({String status = 'all'}) async {
|
||||
final response = await _apiService.get(
|
||||
'/study-plans',
|
||||
queryParameters: {'status': status},
|
||||
);
|
||||
return response['data']['plans'];
|
||||
}
|
||||
|
||||
/// 获取今日学习计划
|
||||
Future<List<dynamic>> getTodayStudyPlans() async {
|
||||
final response = await _apiService.get('/study-plans/today');
|
||||
return response['data']['plans'];
|
||||
}
|
||||
|
||||
/// 获取学习计划详情
|
||||
Future<Map<String, dynamic>> getStudyPlanByID(int planId) async {
|
||||
final response = await _apiService.get('/study-plans/$planId');
|
||||
return response['data'];
|
||||
}
|
||||
|
||||
/// 更新学习计划
|
||||
Future<Map<String, dynamic>> updateStudyPlan(
|
||||
int planId,
|
||||
Map<String, dynamic> updates,
|
||||
) async {
|
||||
final response = await _apiService.put('/study-plans/$planId', data: updates);
|
||||
return response['data'];
|
||||
}
|
||||
|
||||
/// 删除学习计划
|
||||
Future<void> deleteStudyPlan(int planId) async {
|
||||
await _apiService.delete('/study-plans/$planId');
|
||||
}
|
||||
|
||||
/// 更新计划状态
|
||||
Future<void> updatePlanStatus(int planId, String status) async {
|
||||
await _apiService.patch(
|
||||
'/study-plans/$planId/status',
|
||||
data: {'status': status},
|
||||
);
|
||||
}
|
||||
|
||||
/// 记录学习进度
|
||||
Future<void> recordStudyProgress(
|
||||
int planId, {
|
||||
required int wordsStudied,
|
||||
int studyDuration = 0,
|
||||
}) async {
|
||||
await _apiService.post(
|
||||
'/study-plans/$planId/progress',
|
||||
data: {
|
||||
'words_studied': wordsStudied,
|
||||
'study_duration': studyDuration,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取计划统计
|
||||
Future<Map<String, dynamic>> getStudyPlanStatistics(int planId) async {
|
||||
final response = await _apiService.get('/study-plans/$planId/statistics');
|
||||
return response['data'];
|
||||
}
|
||||
}
|
||||
|
||||
/// 学习计划服务提供者
|
||||
final studyPlanServiceProvider = Provider<StudyPlanService>((ref) {
|
||||
final apiService = ref.watch(apiServiceProvider);
|
||||
return StudyPlanService(apiService);
|
||||
});
|
||||
479
client/lib/shared/services/vocabulary_service.dart
Normal file
479
client/lib/shared/services/vocabulary_service.dart
Normal file
@@ -0,0 +1,479 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/errors/app_error.dart';
|
||||
import '../models/api_response.dart';
|
||||
import '../models/vocabulary_model.dart';
|
||||
|
||||
/// 词汇服务
|
||||
class VocabularyService {
|
||||
static final VocabularyService _instance = VocabularyService._internal();
|
||||
factory VocabularyService() => _instance;
|
||||
VocabularyService._internal();
|
||||
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
/// 获取词库列表
|
||||
Future<ApiResponse<List<VocabularyBookModel>>> getVocabularyBooks({
|
||||
String? category,
|
||||
String? level,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
};
|
||||
|
||||
if (category != null) queryParams['category'] = category;
|
||||
if (level != null) queryParams['level'] = level;
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/books',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> booksJson = response.data['data']['items'];
|
||||
final books = booksJson
|
||||
.map((json) => VocabularyBookModel.fromJson(json))
|
||||
.toList();
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'Vocabulary books retrieved successfully',
|
||||
data: books,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get vocabulary books',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get vocabulary books: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取词汇列表
|
||||
Future<ApiResponse<PaginatedResponse<VocabularyModel>>> getVocabularies({
|
||||
String? bookId,
|
||||
String? search,
|
||||
String? level,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
};
|
||||
|
||||
if (bookId != null) queryParams['book_id'] = bookId;
|
||||
if (search != null) queryParams['search'] = search;
|
||||
if (level != null) queryParams['level'] = level;
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/words',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final paginatedResponse = PaginatedResponse<VocabularyModel>.fromJson(
|
||||
response.data['data'],
|
||||
(json) => VocabularyModel.fromJson(json),
|
||||
);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'Vocabularies retrieved successfully',
|
||||
data: paginatedResponse,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get vocabularies',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get vocabularies: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取单词详情
|
||||
Future<ApiResponse<VocabularyModel>> getWordDetail(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/vocabulary/words/$wordId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final word = VocabularyModel.fromJson(response.data['data']);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'Word detail retrieved successfully',
|
||||
data: word,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get word detail',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get word detail: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户词汇学习记录
|
||||
Future<ApiResponse<PaginatedResponse<UserVocabularyModel>>> getUserVocabularies({
|
||||
LearningStatus? status,
|
||||
String? bookId,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
};
|
||||
|
||||
if (status != null) queryParams['status'] = status.name;
|
||||
if (bookId != null) queryParams['book_id'] = bookId;
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/user-words',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final paginatedResponse = PaginatedResponse<UserVocabularyModel>.fromJson(
|
||||
response.data['data'],
|
||||
(json) => UserVocabularyModel.fromJson(json),
|
||||
);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'User vocabularies retrieved successfully',
|
||||
data: paginatedResponse,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get user vocabularies',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get user vocabularies: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新单词学习状态
|
||||
Future<ApiResponse<UserVocabularyModel>> updateWordStatus({
|
||||
required String wordId,
|
||||
required LearningStatus status,
|
||||
int? correctCount,
|
||||
int? wrongCount,
|
||||
int? reviewCount,
|
||||
DateTime? nextReviewDate,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{
|
||||
'status': status.name,
|
||||
};
|
||||
|
||||
if (correctCount != null) data['correct_count'] = correctCount;
|
||||
if (wrongCount != null) data['wrong_count'] = wrongCount;
|
||||
if (reviewCount != null) data['review_count'] = reviewCount;
|
||||
if (nextReviewDate != null) {
|
||||
data['next_review_date'] = nextReviewDate.toIso8601String();
|
||||
}
|
||||
if (metadata != null) data['metadata'] = metadata;
|
||||
|
||||
final response = await _apiClient.put(
|
||||
'/vocabulary/user-words/$wordId',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final userWord = UserVocabularyModel.fromJson(response.data['data']);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? 'Word status updated successfully',
|
||||
data: userWord,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to update word status',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to update word status: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加单词到学习列表
|
||||
Future<ApiResponse<UserVocabularyModel>> addWordToLearning(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/user-words',
|
||||
data: {'word_id': wordId},
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final userWord = UserVocabularyModel.fromJson(response.data['data']);
|
||||
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? 'Word added to learning list',
|
||||
data: userWord,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to add word to learning list',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to add word to learning list: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 从学习列表移除单词
|
||||
Future<ApiResponse<void>> removeWordFromLearning(String wordId) async {
|
||||
try {
|
||||
final response = await _apiClient.delete('/vocabulary/user-words/$wordId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: response.data['message'] ?? 'Word removed from learning list',
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to remove word from learning list',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to remove word from learning list: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取今日复习单词
|
||||
Future<ApiResponse<List<UserVocabularyModel>>> getTodayReviewWords() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/vocabulary/today-review');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> wordsJson = response.data['data'];
|
||||
final words = wordsJson
|
||||
.map((json) => UserVocabularyModel.fromJson(json))
|
||||
.toList();
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'Today review words retrieved successfully',
|
||||
data: words,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get today review words',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get today review words: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取新单词学习
|
||||
Future<ApiResponse<List<VocabularyModel>>> getNewWordsForLearning({
|
||||
String? bookId,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'limit': limit,
|
||||
};
|
||||
|
||||
if (bookId != null) queryParams['book_id'] = bookId;
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/new-words',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> wordsJson = response.data['data'];
|
||||
final words = wordsJson
|
||||
.map((json) => VocabularyModel.fromJson(json))
|
||||
.toList();
|
||||
|
||||
return ApiResponse.success(
|
||||
message: 'New words for learning retrieved successfully',
|
||||
data: words,
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get new words for learning',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get new words for learning: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 词汇量测试
|
||||
Future<ApiResponse<Map<String, dynamic>>> vocabularyTest({
|
||||
required List<String> wordIds,
|
||||
required List<String> answers,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/vocabulary/test',
|
||||
data: {
|
||||
'word_ids': wordIds,
|
||||
'answers': answers,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: 'Vocabulary test completed successfully',
|
||||
data: response.data['data'],
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Vocabulary test failed',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Vocabulary test failed: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取学习统计
|
||||
Future<ApiResponse<Map<String, dynamic>>> getLearningStats({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
|
||||
if (startDate != null) {
|
||||
queryParams['start_date'] = startDate.toIso8601String();
|
||||
}
|
||||
if (endDate != null) {
|
||||
queryParams['end_date'] = endDate.toIso8601String();
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/vocabulary/stats',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: 'Learning stats retrieved successfully',
|
||||
data: response.data['data'],
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.failure(
|
||||
message: response.data['message'] ?? 'Failed to get learning stats',
|
||||
code: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return _handleDioError(e);
|
||||
} catch (e) {
|
||||
return ApiResponse.failure(
|
||||
message: 'Failed to get learning stats: $e',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理Dio错误
|
||||
ApiResponse<T> _handleDioError<T>(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return ApiResponse.failure(
|
||||
message: '请求超时,请检查网络连接',
|
||||
error: 'TIMEOUT',
|
||||
);
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final message = e.response?.data?['message'] ?? '请求失败';
|
||||
return ApiResponse.failure(
|
||||
message: message,
|
||||
code: statusCode,
|
||||
error: 'BAD_RESPONSE',
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return ApiResponse.failure(
|
||||
message: '请求已取消',
|
||||
error: 'CANCELLED',
|
||||
);
|
||||
case DioExceptionType.connectionError:
|
||||
return ApiResponse.failure(
|
||||
message: '网络连接失败,请检查网络设置',
|
||||
error: 'CONNECTION_ERROR',
|
||||
);
|
||||
default:
|
||||
return ApiResponse.failure(
|
||||
message: '未知错误:${e.message}',
|
||||
error: 'UNKNOWN',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
client/lib/shared/services/word_book_service.dart
Normal file
65
client/lib/shared/services/word_book_service.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
|
||||
/// 生词本服务
|
||||
class WordBookService {
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
WordBookService();
|
||||
|
||||
/// 切换单词收藏状态
|
||||
Future<Map<String, dynamic>> toggleFavorite(int wordId) async {
|
||||
final response = await _apiClient.post('/word-book/toggle/$wordId');
|
||||
return response.data['data'];
|
||||
}
|
||||
|
||||
/// 获取生词本列表
|
||||
Future<Map<String, dynamic>> getFavoriteWords({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String sortBy = 'created_at',
|
||||
String order = 'desc',
|
||||
}) async {
|
||||
final response = await _apiClient.get(
|
||||
'/word-book',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
'sort_by': sortBy,
|
||||
'order': order,
|
||||
},
|
||||
);
|
||||
return response.data['data'];
|
||||
}
|
||||
|
||||
/// 获取指定词汇书的生词本
|
||||
Future<List<dynamic>> getFavoriteWordsByBook(String bookId) async {
|
||||
final response = await _apiClient.get('/word-book/books/$bookId');
|
||||
return response.data['data']['words'];
|
||||
}
|
||||
|
||||
/// 获取生词本统计信息
|
||||
Future<Map<String, dynamic>> getFavoriteStats() async {
|
||||
final response = await _apiClient.get('/word-book/stats');
|
||||
return response.data['data'];
|
||||
}
|
||||
|
||||
/// 批量添加到生词本
|
||||
Future<int> batchAddToFavorite(List<int> wordIds) async {
|
||||
final response = await _apiClient.post(
|
||||
'/word-book/batch',
|
||||
data: {'word_ids': wordIds},
|
||||
);
|
||||
return response.data['data']['added_count'];
|
||||
}
|
||||
|
||||
/// 从生词本移除单词
|
||||
Future<void> removeFromFavorite(int wordId) async {
|
||||
await _apiClient.delete('/word-book/$wordId');
|
||||
}
|
||||
}
|
||||
|
||||
/// 生词本服务提供者
|
||||
final wordBookServiceProvider = Provider<WordBookService>((ref) {
|
||||
return WordBookService();
|
||||
});
|
||||
Reference in New Issue
Block a user