init
This commit is contained in:
284
client/lib/core/services/api_service.dart
Normal file
284
client/lib/core/services/api_service.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'storage_service.dart';
|
||||
import '../config/environment.dart';
|
||||
|
||||
class ApiResponse {
|
||||
final dynamic data;
|
||||
final int statusCode;
|
||||
final String? message;
|
||||
|
||||
ApiResponse({
|
||||
required this.data,
|
||||
required this.statusCode,
|
||||
this.message,
|
||||
});
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
late final Dio _dio;
|
||||
final StorageService _storageService;
|
||||
|
||||
ApiService({required StorageService storageService})
|
||||
: _storageService = storageService {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: EnvironmentConfig.baseUrl,
|
||||
connectTimeout: Duration(milliseconds: EnvironmentConfig.connectTimeout),
|
||||
receiveTimeout: Duration(milliseconds: EnvironmentConfig.receiveTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_setupInterceptors();
|
||||
}
|
||||
|
||||
void _setupInterceptors() {
|
||||
// 请求拦截器
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// 添加认证token
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('API Request: ${options.method} ${options.uri}');
|
||||
print('Headers: ${options.headers}');
|
||||
if (options.data != null) {
|
||||
print('Data: ${options.data}');
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
if (kDebugMode) {
|
||||
print('API Response: ${response.statusCode} ${response.requestOptions.uri}');
|
||||
}
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (kDebugMode) {
|
||||
print('API Error: ${error.message}');
|
||||
print('Response: ${error.response?.data}');
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// GET请求
|
||||
Future<ApiResponse> get(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
path,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// POST请求
|
||||
Future<ApiResponse> post(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
Future<ApiResponse> put(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH请求
|
||||
Future<ApiResponse> patch(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.patch(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
Future<ApiResponse> delete(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.delete(
|
||||
path,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
Future<ApiResponse> uploadFile(
|
||||
String path,
|
||||
String filePath, {
|
||||
String? fileName,
|
||||
Map<String, dynamic>? data,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: fileName,
|
||||
),
|
||||
...?data,
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
path,
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
),
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
Future<void> downloadFile(
|
||||
String url,
|
||||
String savePath, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
await _dio.download(
|
||||
url,
|
||||
savePath,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
Exception _handleError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return Exception('网络连接超时,请检查网络设置');
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
final message = error.response?.data?['message'] ?? '请求失败';
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return Exception('请求参数错误: $message');
|
||||
case 401:
|
||||
return Exception('认证失败,请重新登录');
|
||||
case 403:
|
||||
return Exception('权限不足: $message');
|
||||
case 404:
|
||||
return Exception('请求的资源不存在');
|
||||
case 500:
|
||||
return Exception('服务器内部错误,请稍后重试');
|
||||
default:
|
||||
return Exception('请求失败($statusCode): $message');
|
||||
}
|
||||
case DioExceptionType.cancel:
|
||||
return Exception('请求已取消');
|
||||
case DioExceptionType.connectionError:
|
||||
return Exception('网络连接失败,请检查网络设置');
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return Exception('未知错误: ${error.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消所有请求
|
||||
void cancelRequests() {
|
||||
_dio.close();
|
||||
}
|
||||
}
|
||||
378
client/lib/core/services/audio_service.dart
Normal file
378
client/lib/core/services/audio_service.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// 音频录制状态
|
||||
enum RecordingState {
|
||||
idle,
|
||||
recording,
|
||||
paused,
|
||||
stopped,
|
||||
}
|
||||
|
||||
// 音频播放状态
|
||||
enum PlaybackState {
|
||||
idle,
|
||||
playing,
|
||||
paused,
|
||||
stopped,
|
||||
}
|
||||
|
||||
class AudioService {
|
||||
// 录制相关
|
||||
RecordingState _recordingState = RecordingState.idle;
|
||||
String? _currentRecordingPath;
|
||||
DateTime? _recordingStartTime;
|
||||
|
||||
// 播放相关
|
||||
PlaybackState _playbackState = PlaybackState.idle;
|
||||
String? _currentPlayingPath;
|
||||
|
||||
// 回调函数
|
||||
Function(RecordingState)? onRecordingStateChanged;
|
||||
Function(PlaybackState)? onPlaybackStateChanged;
|
||||
Function(Duration)? onRecordingProgress;
|
||||
Function(Duration)? onPlaybackProgress;
|
||||
Function(String)? onRecordingComplete;
|
||||
Function()? onPlaybackComplete;
|
||||
|
||||
// Getters
|
||||
RecordingState get recordingState => _recordingState;
|
||||
PlaybackState get playbackState => _playbackState;
|
||||
String? get currentRecordingPath => _currentRecordingPath;
|
||||
String? get currentPlayingPath => _currentPlayingPath;
|
||||
bool get isRecording => _recordingState == RecordingState.recording;
|
||||
bool get isPlaying => _playbackState == PlaybackState.playing;
|
||||
|
||||
// 初始化音频服务
|
||||
Future<void> initialize() async {
|
||||
// 请求麦克风权限
|
||||
await _requestPermissions();
|
||||
}
|
||||
|
||||
// 请求权限
|
||||
Future<bool> _requestPermissions() async {
|
||||
// Web平台不支持某些权限,需要特殊处理
|
||||
if (kIsWeb) {
|
||||
// Web平台只需要麦克风权限,且通过浏览器API处理
|
||||
if (kDebugMode) {
|
||||
print('Web平台:跳过权限请求');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final microphoneStatus = await Permission.microphone.request();
|
||||
|
||||
if (microphoneStatus != PermissionStatus.granted) {
|
||||
throw Exception('需要麦克风权限才能录音');
|
||||
}
|
||||
|
||||
// 存储权限在某些平台可能不需要
|
||||
try {
|
||||
final storageStatus = await Permission.storage.request();
|
||||
if (storageStatus != PermissionStatus.granted) {
|
||||
if (kDebugMode) {
|
||||
print('存储权限未授予,但继续执行');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 某些平台不支持存储权限,忽略错误
|
||||
if (kDebugMode) {
|
||||
print('存储权限请求失败(可能不支持): $e');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('权限请求失败: $e');
|
||||
}
|
||||
// 在某些平台上,权限请求可能失败,但仍然可以继续
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
Future<void> startRecording({String? fileName}) async {
|
||||
try {
|
||||
if (_recordingState == RecordingState.recording) {
|
||||
throw Exception('已经在录音中');
|
||||
}
|
||||
|
||||
await _requestPermissions();
|
||||
|
||||
// 生成录音文件路径
|
||||
if (kIsWeb) {
|
||||
// Web平台使用内存存储或IndexedDB
|
||||
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.webm';
|
||||
_currentRecordingPath = '/recordings/$fileName';
|
||||
} else {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final recordingsDir = Directory('${directory.path}/recordings');
|
||||
if (!await recordingsDir.exists()) {
|
||||
await recordingsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
_currentRecordingPath = '${recordingsDir.path}/$fileName';
|
||||
}
|
||||
|
||||
// 这里应该使用实际的录音插件,比如 record 或 flutter_sound
|
||||
// 由于没有实际的录音插件,这里只是模拟
|
||||
_recordingStartTime = DateTime.now();
|
||||
_setRecordingState(RecordingState.recording);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('开始录音: $_currentRecordingPath');
|
||||
}
|
||||
|
||||
// 模拟录音进度更新
|
||||
_startRecordingProgressTimer();
|
||||
|
||||
} catch (e) {
|
||||
throw Exception('开始录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止录音
|
||||
Future<String?> stopRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.recording) {
|
||||
throw Exception('当前没有在录音');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的停止方法
|
||||
_setRecordingState(RecordingState.stopped);
|
||||
|
||||
final recordingPath = _currentRecordingPath;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音完成: $recordingPath');
|
||||
}
|
||||
|
||||
// 通知录音完成
|
||||
if (recordingPath != null && onRecordingComplete != null) {
|
||||
onRecordingComplete!(recordingPath);
|
||||
}
|
||||
|
||||
return recordingPath;
|
||||
} catch (e) {
|
||||
throw Exception('停止录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停录音
|
||||
Future<void> pauseRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.recording) {
|
||||
throw Exception('当前没有在录音');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的暂停方法
|
||||
_setRecordingState(RecordingState.paused);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音已暂停');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('暂停录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复录音
|
||||
Future<void> resumeRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.paused) {
|
||||
throw Exception('录音没有暂停');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的恢复方法
|
||||
_setRecordingState(RecordingState.recording);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音已恢复');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('恢复录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
Future<void> playAudio(String audioPath) async {
|
||||
try {
|
||||
if (_playbackState == PlaybackState.playing) {
|
||||
await stopPlayback();
|
||||
}
|
||||
|
||||
_currentPlayingPath = audioPath;
|
||||
|
||||
// 这里应该使用实际的音频播放插件,比如 audioplayers 或 just_audio
|
||||
// 由于没有实际的播放插件,这里只是模拟
|
||||
_setPlaybackState(PlaybackState.playing);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('开始播放: $audioPath');
|
||||
}
|
||||
|
||||
// 模拟播放进度和完成
|
||||
_startPlaybackProgressTimer();
|
||||
|
||||
} catch (e) {
|
||||
throw Exception('播放音频失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停播放
|
||||
Future<void> pausePlayback() async {
|
||||
try {
|
||||
if (_playbackState != PlaybackState.playing) {
|
||||
throw Exception('当前没有在播放');
|
||||
}
|
||||
|
||||
// 这里应该调用实际播放插件的暂停方法
|
||||
_setPlaybackState(PlaybackState.paused);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已暂停');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('暂停播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复播放
|
||||
Future<void> resumePlayback() async {
|
||||
try {
|
||||
if (_playbackState != PlaybackState.paused) {
|
||||
throw Exception('播放没有暂停');
|
||||
}
|
||||
|
||||
// 这里应该调用实际播放插件的恢复方法
|
||||
_setPlaybackState(PlaybackState.playing);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已恢复');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('恢复播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止播放
|
||||
Future<void> stopPlayback() async {
|
||||
try {
|
||||
// 这里应该调用实际播放插件的停止方法
|
||||
_setPlaybackState(PlaybackState.stopped);
|
||||
_currentPlayingPath = null;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已停止');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('停止播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取音频文件时长
|
||||
Future<Duration?> getAudioDuration(String audioPath) async {
|
||||
try {
|
||||
// 这里应该使用实际的音频插件获取时长
|
||||
// 模拟返回时长
|
||||
return const Duration(seconds: 30);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('获取音频时长失败: ${e.toString()}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除录音文件
|
||||
Future<bool> deleteRecording(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('删除录音文件失败: ${e.toString()}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有录音文件
|
||||
Future<List<String>> getAllRecordings() async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final recordingsDir = Directory('${directory.path}/recordings');
|
||||
|
||||
if (!await recordingsDir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = await recordingsDir.list().toList();
|
||||
return files
|
||||
.where((file) => file is File && file.path.endsWith('.m4a'))
|
||||
.map((file) => file.path)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('获取录音文件列表失败: ${e.toString()}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 私有方法:设置录音状态
|
||||
void _setRecordingState(RecordingState state) {
|
||||
_recordingState = state;
|
||||
onRecordingStateChanged?.call(state);
|
||||
}
|
||||
|
||||
// 私有方法:设置播放状态
|
||||
void _setPlaybackState(PlaybackState state) {
|
||||
_playbackState = state;
|
||||
onPlaybackStateChanged?.call(state);
|
||||
}
|
||||
|
||||
// 私有方法:录音进度计时器
|
||||
void _startRecordingProgressTimer() {
|
||||
// 这里应该实现实际的进度更新逻辑
|
||||
// 模拟进度更新
|
||||
}
|
||||
|
||||
// 私有方法:播放进度计时器
|
||||
void _startPlaybackProgressTimer() {
|
||||
// 这里应该实现实际的播放进度更新逻辑
|
||||
// 模拟播放完成
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
_setPlaybackState(PlaybackState.stopped);
|
||||
onPlaybackComplete?.call();
|
||||
});
|
||||
}
|
||||
|
||||
// 释放资源
|
||||
void dispose() {
|
||||
// 停止所有操作
|
||||
if (_recordingState == RecordingState.recording) {
|
||||
stopRecording();
|
||||
}
|
||||
if (_playbackState == PlaybackState.playing) {
|
||||
stopPlayback();
|
||||
}
|
||||
|
||||
// 清理回调
|
||||
onRecordingStateChanged = null;
|
||||
onPlaybackStateChanged = null;
|
||||
onRecordingProgress = null;
|
||||
onPlaybackProgress = null;
|
||||
onRecordingComplete = null;
|
||||
onPlaybackComplete = null;
|
||||
}
|
||||
}
|
||||
327
client/lib/core/services/auth_service.dart
Normal file
327
client/lib/core/services/auth_service.dart
Normal file
@@ -0,0 +1,327 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/user_model.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../network/api_endpoints.dart';
|
||||
import '../errors/app_exception.dart';
|
||||
|
||||
/// 认证服务
|
||||
class AuthService {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
AuthService(this._apiClient);
|
||||
|
||||
/// 登录
|
||||
Future<AuthResponse> login({
|
||||
required String account, // 用户名或邮箱
|
||||
required String password,
|
||||
bool rememberMe = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.login,
|
||||
data: {
|
||||
'account': account,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
|
||||
// 后端返回格式: {code, message, data: {user, access_token, refresh_token, expires_in}}
|
||||
final data = response.data['data'];
|
||||
final userInfo = data['user'];
|
||||
|
||||
return AuthResponse(
|
||||
user: User(
|
||||
id: userInfo['id'].toString(),
|
||||
username: userInfo['username'],
|
||||
email: userInfo['email'],
|
||||
avatar: userInfo['avatar'],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
token: data['access_token'],
|
||||
refreshToken: data['refresh_token'],
|
||||
expiresAt: DateTime.now().add(Duration(seconds: data['expires_in'])),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('登录失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户注册
|
||||
Future<AuthResponse> register({
|
||||
required String email,
|
||||
required String password,
|
||||
required String username,
|
||||
required String nickname,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.register,
|
||||
data: {
|
||||
'email': email,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'nickname': nickname,
|
||||
},
|
||||
);
|
||||
|
||||
// 后端返回的数据结构需要转换
|
||||
final data = response.data['data'];
|
||||
final userInfo = data['user'];
|
||||
|
||||
return AuthResponse(
|
||||
user: User(
|
||||
id: userInfo['id'].toString(),
|
||||
username: userInfo['username'],
|
||||
email: userInfo['email'],
|
||||
avatar: userInfo['avatar'],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
token: data['access_token'],
|
||||
refreshToken: data['refresh_token'],
|
||||
expiresAt: DateTime.now().add(Duration(seconds: data['expires_in'])),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('注册失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 第三方登录
|
||||
Future<AuthResponse> socialLogin({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.socialLogin,
|
||||
data: {
|
||||
'provider': provider,
|
||||
'access_token': accessToken,
|
||||
},
|
||||
);
|
||||
|
||||
return AuthResponse.fromJson(response.data);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('第三方登录失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 忘记密码
|
||||
Future<void> forgotPassword(String email) async {
|
||||
try {
|
||||
await _apiClient.post(
|
||||
ApiEndpoints.forgotPassword,
|
||||
data: {'email': email},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('发送重置密码邮件失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置密码
|
||||
Future<void> resetPassword({
|
||||
required String token,
|
||||
required String newPassword,
|
||||
required String confirmPassword,
|
||||
}) async {
|
||||
try {
|
||||
await _apiClient.post(
|
||||
ApiEndpoints.resetPassword,
|
||||
data: {
|
||||
'token': token,
|
||||
'new_password': newPassword,
|
||||
'confirm_password': confirmPassword,
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('重置密码失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改密码
|
||||
Future<void> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
required String confirmPassword,
|
||||
}) async {
|
||||
try {
|
||||
await _apiClient.put(
|
||||
ApiEndpoints.changePassword,
|
||||
data: {
|
||||
'current_password': currentPassword,
|
||||
'new_password': newPassword,
|
||||
'confirm_password': confirmPassword,
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('修改密码失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新Token
|
||||
Future<TokenRefreshResponse> refreshToken(String refreshToken) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.refreshToken,
|
||||
data: {'refresh_token': refreshToken},
|
||||
);
|
||||
|
||||
return TokenRefreshResponse.fromJson(response.data);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('刷新Token失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 登出
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _apiClient.post(ApiEndpoints.logout);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('登出失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户信息
|
||||
Future<User> getUserInfo() async {
|
||||
try {
|
||||
final response = await _apiClient.get(ApiEndpoints.userInfo);
|
||||
return User.fromJson(response.data);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('获取用户信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前用户信息(getUserInfo的别名)
|
||||
Future<User> getCurrentUser() async {
|
||||
return await getUserInfo();
|
||||
}
|
||||
|
||||
/// 更新用户信息
|
||||
Future<User> updateUserInfo(Map<String, dynamic> data) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
ApiEndpoints.userInfo,
|
||||
data: data,
|
||||
);
|
||||
return User.fromJson(response.data);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('更新用户信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证邮箱
|
||||
Future<void> verifyEmail(String token) async {
|
||||
try {
|
||||
await _apiClient.post(
|
||||
ApiEndpoints.verifyEmail,
|
||||
data: {'token': token},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('验证邮箱失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新发送验证邮件
|
||||
Future<void> resendVerificationEmail() async {
|
||||
try {
|
||||
await _apiClient.post(ApiEndpoints.resendVerificationEmail);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('发送验证邮件失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查用户名是否可用
|
||||
Future<bool> checkUsernameAvailability(String username) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiEndpoints.checkUsername}?username=$username',
|
||||
);
|
||||
return response.data['available'] ?? false;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('检查用户名失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查邮箱是否可用
|
||||
Future<bool> checkEmailAvailability(String email) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiEndpoints.checkEmail}?email=$email',
|
||||
);
|
||||
return response.data['available'] ?? false;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e) {
|
||||
throw AppException('检查邮箱失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理Dio异常
|
||||
AppException _handleDioException(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return NetworkException('连接超时');
|
||||
case DioExceptionType.sendTimeout:
|
||||
return NetworkException('发送超时');
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return NetworkException('接收超时');
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final message = e.response?.data?['message'] ?? '请求失败';
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return ValidationException(message);
|
||||
case 401:
|
||||
return AuthException('认证失败');
|
||||
case 403:
|
||||
return AuthException('权限不足');
|
||||
case 404:
|
||||
return AppException('资源不存在');
|
||||
case 422:
|
||||
return ValidationException(message);
|
||||
case 500:
|
||||
return ServerException('服务器内部错误');
|
||||
default:
|
||||
return AppException('请求失败: $message');
|
||||
}
|
||||
case DioExceptionType.cancel:
|
||||
return AppException('请求已取消');
|
||||
case DioExceptionType.connectionError:
|
||||
return NetworkException('网络连接错误');
|
||||
case DioExceptionType.badCertificate:
|
||||
return NetworkException('证书错误');
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return AppException('未知错误: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
252
client/lib/core/services/cache_service.dart
Normal file
252
client/lib/core/services/cache_service.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../storage/storage_service.dart';
|
||||
|
||||
/// API数据缓存服务
|
||||
class CacheService {
|
||||
static const String _cachePrefix = 'api_cache_';
|
||||
static const String _timestampPrefix = 'cache_timestamp_';
|
||||
static const Duration _defaultCacheDuration = Duration(minutes: 30);
|
||||
|
||||
/// 设置缓存
|
||||
static Future<void> setCache(
|
||||
String key,
|
||||
dynamic data, {
|
||||
Duration? duration,
|
||||
}) async {
|
||||
try {
|
||||
final cacheKey = _cachePrefix + key;
|
||||
final timestampKey = _timestampPrefix + key;
|
||||
|
||||
// 存储数据
|
||||
final jsonData = jsonEncode(data);
|
||||
await StorageService.setString(cacheKey, jsonData);
|
||||
|
||||
// 存储时间戳
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
await StorageService.setInt(timestampKey, timestamp);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Cache set for key: $key');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error setting cache for key $key: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取缓存
|
||||
static Future<T?> getCache<T>(
|
||||
String key, {
|
||||
Duration? duration,
|
||||
T Function(Map<String, dynamic>)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final cacheKey = _cachePrefix + key;
|
||||
final timestampKey = _timestampPrefix + key;
|
||||
|
||||
// 检查缓存是否存在
|
||||
final cachedData = await StorageService.getString(cacheKey);
|
||||
final timestamp = await StorageService.getInt(timestampKey);
|
||||
|
||||
if (cachedData == null || timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查缓存是否过期
|
||||
final cacheDuration = duration ?? _defaultCacheDuration;
|
||||
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.difference(cacheTime) > cacheDuration) {
|
||||
// 缓存过期,删除缓存
|
||||
await removeCache(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
final jsonData = jsonDecode(cachedData);
|
||||
|
||||
if (fromJson != null && jsonData is Map<String, dynamic>) {
|
||||
return fromJson(jsonData);
|
||||
}
|
||||
|
||||
return jsonData as T;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error getting cache for key $key: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取列表缓存
|
||||
static Future<List<T>?> getListCache<T>(
|
||||
String key, {
|
||||
Duration? duration,
|
||||
required T Function(Map<String, dynamic>) fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final cacheKey = _cachePrefix + key;
|
||||
final timestampKey = _timestampPrefix + key;
|
||||
|
||||
// 检查缓存是否存在
|
||||
final cachedData = await StorageService.getString(cacheKey);
|
||||
final timestamp = await StorageService.getInt(timestampKey);
|
||||
|
||||
if (cachedData == null || timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查缓存是否过期
|
||||
final cacheDuration = duration ?? _defaultCacheDuration;
|
||||
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.difference(cacheTime) > cacheDuration) {
|
||||
// 缓存过期,删除缓存
|
||||
await removeCache(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
final jsonData = jsonDecode(cachedData);
|
||||
|
||||
if (jsonData is List) {
|
||||
return jsonData
|
||||
.map((item) => fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error getting list cache for key $key: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查缓存是否有效
|
||||
static Future<bool> isCacheValid(
|
||||
String key, {
|
||||
Duration? duration,
|
||||
}) async {
|
||||
try {
|
||||
final timestampKey = _timestampPrefix + key;
|
||||
final timestamp = await StorageService.getInt(timestampKey);
|
||||
|
||||
if (timestamp == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final cacheDuration = duration ?? _defaultCacheDuration;
|
||||
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final now = DateTime.now();
|
||||
|
||||
return now.difference(cacheTime) <= cacheDuration;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除缓存
|
||||
static Future<void> removeCache(String key) async {
|
||||
try {
|
||||
final cacheKey = _cachePrefix + key;
|
||||
final timestampKey = _timestampPrefix + key;
|
||||
|
||||
await StorageService.remove(cacheKey);
|
||||
await StorageService.remove(timestampKey);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Cache removed for key: $key');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error removing cache for key $key: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有缓存
|
||||
static Future<void> clearAllCache() async {
|
||||
try {
|
||||
final keys = StorageService.getKeys();
|
||||
final cacheKeys = keys.where((key) =>
|
||||
key.startsWith(_cachePrefix) || key.startsWith(_timestampPrefix));
|
||||
|
||||
for (final key in cacheKeys) {
|
||||
await StorageService.remove(key);
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('All cache cleared');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error clearing all cache: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取缓存大小信息
|
||||
static Future<Map<String, int>> getCacheInfo() async {
|
||||
try {
|
||||
final keys = StorageService.getKeys();
|
||||
final cacheKeys = keys.where((key) => key.startsWith(_cachePrefix));
|
||||
final timestampKeys = keys.where((key) => key.startsWith(_timestampPrefix));
|
||||
|
||||
int totalSize = 0;
|
||||
for (final key in cacheKeys) {
|
||||
final data = await StorageService.getString(key);
|
||||
if (data != null) {
|
||||
totalSize += data.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'count': cacheKeys.length,
|
||||
'size': totalSize,
|
||||
'timestamps': timestampKeys.length,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
'count': 0,
|
||||
'size': 0,
|
||||
'timestamps': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理过期缓存
|
||||
static Future<void> cleanExpiredCache() async {
|
||||
try {
|
||||
final keys = StorageService.getKeys();
|
||||
final timestampKeys = keys.where((key) => key.startsWith(_timestampPrefix));
|
||||
|
||||
for (final timestampKey in timestampKeys) {
|
||||
final timestamp = await StorageService.getInt(timestampKey);
|
||||
if (timestamp != null) {
|
||||
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.difference(cacheTime) > _defaultCacheDuration) {
|
||||
final cacheKey = timestampKey.replaceFirst(_timestampPrefix, _cachePrefix);
|
||||
await StorageService.remove(cacheKey);
|
||||
await StorageService.remove(timestampKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Expired cache cleaned');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error cleaning expired cache: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
288
client/lib/core/services/enhanced_api_service.dart
Normal file
288
client/lib/core/services/enhanced_api_service.dart
Normal file
@@ -0,0 +1,288 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../models/api_response.dart';
|
||||
import '../storage/storage_service.dart';
|
||||
|
||||
/// 增强版API服务,集成缓存功能
|
||||
class EnhancedApiService {
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
/// GET请求,支持缓存
|
||||
Future<ApiResponse<T>> get<T>(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
bool useCache = true,
|
||||
Duration? cacheDuration,
|
||||
T Function(Map<String, dynamic>)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
// 生成缓存键
|
||||
final cacheKey = _generateCacheKey('GET', endpoint, queryParameters);
|
||||
|
||||
// 尝试从缓存获取数据
|
||||
if (useCache) {
|
||||
final cachedData = await CacheService.getCache<T>(
|
||||
cacheKey,
|
||||
duration: cacheDuration,
|
||||
fromJson: fromJson,
|
||||
);
|
||||
|
||||
if (cachedData != null) {
|
||||
if (kDebugMode) {
|
||||
print('Cache hit for: $endpoint');
|
||||
}
|
||||
return ApiResponse.success(
|
||||
data: cachedData,
|
||||
message: 'Data from cache',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 发起网络请求
|
||||
final response = await _apiClient.get(
|
||||
endpoint,
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
// 缓存成功响应的数据
|
||||
if (useCache) {
|
||||
await CacheService.setCache(
|
||||
cacheKey,
|
||||
response.data,
|
||||
duration: cacheDuration,
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: fromJson != null ? fromJson(response.data) : response.data,
|
||||
message: 'Success',
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse.error(
|
||||
message: 'Request failed with status: ${response.statusCode}',
|
||||
code: response.statusCode,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error in enhanced GET request: $e');
|
||||
}
|
||||
return ApiResponse.error(
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST请求
|
||||
Future<ApiResponse<T>> post<T>(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? data,
|
||||
bool invalidateCache = true,
|
||||
List<String>? cacheKeysToInvalidate,
|
||||
T Function(Map<String, dynamic>)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
endpoint,
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// POST请求成功后,清除相关缓存
|
||||
if (invalidateCache) {
|
||||
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: fromJson != null && response.data != null ? fromJson(response.data) : response.data,
|
||||
message: 'Success',
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse.error(
|
||||
message: 'Request failed with status: ${response.statusCode}',
|
||||
code: response.statusCode,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error in enhanced POST request: $e');
|
||||
}
|
||||
return ApiResponse.error(
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT请求
|
||||
Future<ApiResponse<T>> put<T>(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? data,
|
||||
bool invalidateCache = true,
|
||||
List<String>? cacheKeysToInvalidate,
|
||||
T Function(Map<String, dynamic>)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
endpoint,
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// PUT请求成功后,清除相关缓存
|
||||
if (invalidateCache) {
|
||||
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: fromJson != null && response.data != null ? fromJson(response.data) : response.data,
|
||||
message: 'Success',
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse.error(
|
||||
message: 'Request failed with status: ${response.statusCode}',
|
||||
code: response.statusCode,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error in enhanced PUT request: $e');
|
||||
}
|
||||
return ApiResponse.error(
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE请求
|
||||
Future<ApiResponse<T>> delete<T>(
|
||||
String endpoint, {
|
||||
bool invalidateCache = true,
|
||||
List<String>? cacheKeysToInvalidate,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.delete(
|
||||
endpoint,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
// DELETE请求成功后,清除相关缓存
|
||||
if (invalidateCache) {
|
||||
await _invalidateRelatedCache(endpoint, cacheKeysToInvalidate);
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: null,
|
||||
message: 'Success',
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse.error(
|
||||
message: 'Request failed with status: ${response.statusCode}',
|
||||
code: response.statusCode,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error in enhanced DELETE request: $e');
|
||||
}
|
||||
return ApiResponse.error(
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成缓存键
|
||||
String _generateCacheKey(
|
||||
String method,
|
||||
String endpoint,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.write(method);
|
||||
buffer.write('_');
|
||||
buffer.write(endpoint.replaceAll('/', '_'));
|
||||
|
||||
if (queryParameters != null && queryParameters.isNotEmpty) {
|
||||
final sortedKeys = queryParameters.keys.toList()..sort();
|
||||
for (final key in sortedKeys) {
|
||||
buffer.write('_${key}_${queryParameters[key]}');
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 清除相关缓存
|
||||
Future<void> _invalidateRelatedCache(
|
||||
String endpoint,
|
||||
List<String>? specificKeys,
|
||||
) async {
|
||||
try {
|
||||
// 清除指定的缓存键
|
||||
if (specificKeys != null) {
|
||||
for (final key in specificKeys) {
|
||||
await CacheService.removeCache(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除与当前端点相关的缓存
|
||||
final endpointKey = endpoint.replaceAll('/', '_');
|
||||
final keys = StorageService.getKeys();
|
||||
final relatedKeys = keys.where((key) => key.contains(endpointKey));
|
||||
|
||||
for (final key in relatedKeys) {
|
||||
final cacheKey = key.replaceFirst('api_cache_', '');
|
||||
await CacheService.removeCache(cacheKey);
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Cache invalidated for endpoint: $endpoint');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error invalidating cache: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 预加载数据到缓存
|
||||
Future<void> preloadCache(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Duration? cacheDuration,
|
||||
}) async {
|
||||
try {
|
||||
await get(
|
||||
endpoint,
|
||||
queryParameters: queryParameters,
|
||||
useCache: true,
|
||||
cacheDuration: cacheDuration,
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Cache preloaded for: $endpoint');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error preloading cache: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取缓存信息
|
||||
Future<Map<String, int>> getCacheInfo() async {
|
||||
return await CacheService.getCacheInfo();
|
||||
}
|
||||
|
||||
/// 清理过期缓存
|
||||
Future<void> cleanExpiredCache() async {
|
||||
await CacheService.cleanExpiredCache();
|
||||
}
|
||||
|
||||
/// 清空所有缓存
|
||||
Future<void> clearAllCache() async {
|
||||
await CacheService.clearAllCache();
|
||||
}
|
||||
}
|
||||
125
client/lib/core/services/navigation_service.dart
Normal file
125
client/lib/core/services/navigation_service.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 全局导航服务
|
||||
/// 用于在非Widget上下文中进行页面导航
|
||||
class NavigationService {
|
||||
static final NavigationService _instance = NavigationService._internal();
|
||||
|
||||
factory NavigationService() => _instance;
|
||||
|
||||
NavigationService._internal();
|
||||
|
||||
static NavigationService get instance => _instance;
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
/// 获取当前上下文
|
||||
BuildContext? get context => navigatorKey.currentContext;
|
||||
|
||||
/// 获取Navigator
|
||||
NavigatorState? get navigator => navigatorKey.currentState;
|
||||
|
||||
/// 导航到指定路由
|
||||
Future<T?>? navigateTo<T>(String routeName, {Object? arguments}) {
|
||||
return navigator?.pushNamed<T>(routeName, arguments: arguments);
|
||||
}
|
||||
|
||||
/// 替换当前路由
|
||||
Future<T?>? replaceWith<T extends Object?>(String routeName, {Object? arguments}) {
|
||||
return navigator?.pushReplacementNamed<T, T>(routeName, arguments: arguments);
|
||||
}
|
||||
|
||||
/// 导航到指定路由并清除所有历史
|
||||
Future<T?>? navigateToAndClearStack<T extends Object?>(String routeName, {Object? arguments}) {
|
||||
return navigator?.pushNamedAndRemoveUntil<T>(
|
||||
routeName,
|
||||
(route) => false,
|
||||
arguments: arguments,
|
||||
);
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
void goBack<T>([T? result]) {
|
||||
if (navigator?.canPop() ?? false) {
|
||||
navigator?.pop<T>(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回到指定路由
|
||||
void popUntil(String routeName) {
|
||||
navigator?.popUntil(ModalRoute.withName(routeName));
|
||||
}
|
||||
|
||||
/// 显示SnackBar
|
||||
void showSnackBar(String message, {
|
||||
Duration duration = const Duration(seconds: 2),
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context!);
|
||||
// 清除之前的所有SnackBar
|
||||
scaffoldMessenger.clearSnackBars();
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: duration,
|
||||
action: action,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示错误SnackBar
|
||||
void showErrorSnackBar(String message) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context!);
|
||||
// 清除之前的所有SnackBar
|
||||
scaffoldMessenger.clearSnackBars();
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示成功SnackBar
|
||||
void showSuccessSnackBar(String message) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context!);
|
||||
// 清除之前的所有SnackBar
|
||||
scaffoldMessenger.clearSnackBars();
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示对话框
|
||||
Future<T?> showDialogWidget<T>(Widget dialog) {
|
||||
return showDialog<T>(
|
||||
context: context!,
|
||||
builder: (context) => dialog,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示底部弹窗
|
||||
Future<T?> showBottomSheetWidget<T>(Widget bottomSheet) {
|
||||
return showModalBottomSheet<T>(
|
||||
context: context!,
|
||||
builder: (context) => bottomSheet,
|
||||
);
|
||||
}
|
||||
}
|
||||
342
client/lib/core/services/storage_service.dart
Normal file
342
client/lib/core/services/storage_service.dart
Normal file
@@ -0,0 +1,342 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/exceptions.dart';
|
||||
|
||||
/// 存储服务
|
||||
class StorageService {
|
||||
static StorageService? _instance;
|
||||
static SharedPreferences? _prefs;
|
||||
|
||||
StorageService._();
|
||||
|
||||
/// 获取单例实例
|
||||
static Future<StorageService> getInstance() async {
|
||||
if (_instance == null) {
|
||||
_instance = StorageService._();
|
||||
await _instance!._init();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 获取已初始化的实例(同步方法,必须先调用getInstance)
|
||||
static StorageService get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('StorageService not initialized. Call getInstance() first.');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 初始化
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
} catch (e) {
|
||||
throw CacheException('初始化存储服务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 普通存储 ====================
|
||||
|
||||
/// 存储字符串
|
||||
Future<bool> setString(String key, String value) async {
|
||||
try {
|
||||
return await _prefs!.setString(key, value);
|
||||
} catch (e) {
|
||||
throw CacheException('存储字符串失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取字符串
|
||||
String? getString(String key, {String? defaultValue}) {
|
||||
try {
|
||||
return _prefs!.getString(key) ?? defaultValue;
|
||||
} catch (e) {
|
||||
throw CacheException('获取字符串失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储整数
|
||||
Future<bool> setInt(String key, int value) async {
|
||||
try {
|
||||
return await _prefs!.setInt(key, value);
|
||||
} catch (e) {
|
||||
throw CacheException('存储整数失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取整数
|
||||
int? getInt(String key, {int? defaultValue}) {
|
||||
try {
|
||||
return _prefs!.getInt(key) ?? defaultValue;
|
||||
} catch (e) {
|
||||
throw CacheException('获取整数失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储布尔值
|
||||
Future<bool> setBool(String key, bool value) async {
|
||||
try {
|
||||
return await _prefs!.setBool(key, value);
|
||||
} catch (e) {
|
||||
throw CacheException('存储布尔值失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取布尔值
|
||||
bool? getBool(String key, {bool? defaultValue}) {
|
||||
try {
|
||||
return _prefs!.getBool(key) ?? defaultValue;
|
||||
} catch (e) {
|
||||
throw CacheException('获取布尔值失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储双精度浮点数
|
||||
Future<bool> setDouble(String key, double value) async {
|
||||
try {
|
||||
return await _prefs!.setDouble(key, value);
|
||||
} catch (e) {
|
||||
throw CacheException('存储双精度浮点数失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取双精度浮点数
|
||||
double? getDouble(String key, {double? defaultValue}) {
|
||||
try {
|
||||
return _prefs!.getDouble(key) ?? defaultValue;
|
||||
} catch (e) {
|
||||
throw CacheException('获取双精度浮点数失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储字符串列表
|
||||
Future<bool> setStringList(String key, List<String> value) async {
|
||||
try {
|
||||
return await _prefs!.setStringList(key, value);
|
||||
} catch (e) {
|
||||
throw CacheException('存储字符串列表失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取字符串列表
|
||||
List<String>? getStringList(String key, {List<String>? defaultValue}) {
|
||||
try {
|
||||
return _prefs!.getStringList(key) ?? defaultValue;
|
||||
} catch (e) {
|
||||
throw CacheException('获取字符串列表失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储JSON对象
|
||||
Future<bool> setJson(String key, Map<String, dynamic> value) async {
|
||||
try {
|
||||
final jsonString = jsonEncode(value);
|
||||
return await setString(key, jsonString);
|
||||
} catch (e) {
|
||||
throw CacheException('存储JSON对象失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取JSON对象
|
||||
Map<String, dynamic>? getJson(String key) {
|
||||
try {
|
||||
final jsonString = getString(key);
|
||||
if (jsonString == null) return null;
|
||||
return jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw CacheException('获取JSON对象失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除指定键的数据
|
||||
Future<bool> remove(String key) async {
|
||||
try {
|
||||
return await _prefs!.remove(key);
|
||||
} catch (e) {
|
||||
throw CacheException('删除数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有数据
|
||||
Future<bool> clear() async {
|
||||
try {
|
||||
return await _prefs!.clear();
|
||||
} catch (e) {
|
||||
throw CacheException('清空数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否包含指定键
|
||||
bool containsKey(String key) {
|
||||
try {
|
||||
return _prefs!.containsKey(key);
|
||||
} catch (e) {
|
||||
throw CacheException('检查键是否存在失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有键
|
||||
Set<String> getKeys() {
|
||||
try {
|
||||
return _prefs!.getKeys();
|
||||
} catch (e) {
|
||||
throw CacheException('获取所有键失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 安全存储 ====================
|
||||
|
||||
/// 安全存储字符串(用于敏感信息如Token)
|
||||
Future<void> setSecureString(String key, String value) async {
|
||||
try {
|
||||
await _prefs!.setString('secure_$key', value);
|
||||
} catch (e) {
|
||||
throw CacheException('安全存储字符串失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全获取字符串
|
||||
Future<String?> getSecureString(String key) async {
|
||||
try {
|
||||
return _prefs!.getString('secure_$key');
|
||||
} catch (e) {
|
||||
throw CacheException('安全获取字符串失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全存储JSON对象
|
||||
Future<void> setSecureJson(String key, Map<String, dynamic> value) async {
|
||||
try {
|
||||
final jsonString = jsonEncode(value);
|
||||
await setSecureString(key, jsonString);
|
||||
} catch (e) {
|
||||
throw CacheException('安全存储JSON对象失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全获取JSON对象
|
||||
Future<Map<String, dynamic>?> getSecureJson(String key) async {
|
||||
try {
|
||||
final jsonString = await getSecureString(key);
|
||||
if (jsonString == null) return null;
|
||||
return jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw CacheException('安全获取JSON对象失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全删除指定键的数据
|
||||
Future<void> removeSecure(String key) async {
|
||||
try {
|
||||
await _prefs!.remove('secure_$key');
|
||||
} catch (e) {
|
||||
throw CacheException('安全删除数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全清空所有数据
|
||||
Future<void> clearSecure() async {
|
||||
try {
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith('secure_')).toList();
|
||||
for (final key in keys) {
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
} catch (e) {
|
||||
throw CacheException('安全清空数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全检查是否包含指定键
|
||||
Future<bool> containsSecureKey(String key) async {
|
||||
try {
|
||||
return _prefs!.containsKey('secure_$key');
|
||||
} catch (e) {
|
||||
throw CacheException('安全检查键是否存在失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全获取所有键
|
||||
Future<Map<String, String>> getAllSecure() async {
|
||||
try {
|
||||
final result = <String, String>{};
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith('secure_'));
|
||||
for (final key in keys) {
|
||||
final value = _prefs!.getString(key);
|
||||
if (value != null) {
|
||||
result[key.substring(7)] = value; // 移除 'secure_' 前缀
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw CacheException('安全获取所有数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Token 相关便捷方法 ====================
|
||||
|
||||
/// 保存访问令牌
|
||||
Future<void> saveToken(String token) async {
|
||||
await setSecureString(StorageKeys.accessToken, token);
|
||||
}
|
||||
|
||||
/// 获取访问令牌
|
||||
Future<String?> getToken() async {
|
||||
return await getSecureString(StorageKeys.accessToken);
|
||||
}
|
||||
|
||||
/// 保存刷新令牌
|
||||
Future<void> saveRefreshToken(String refreshToken) async {
|
||||
await setSecureString(StorageKeys.refreshToken, refreshToken);
|
||||
}
|
||||
|
||||
/// 获取刷新令牌
|
||||
Future<String?> getRefreshToken() async {
|
||||
return await getSecureString(StorageKeys.refreshToken);
|
||||
}
|
||||
|
||||
/// 清除所有令牌
|
||||
Future<void> clearTokens() async {
|
||||
await removeSecure(StorageKeys.accessToken);
|
||||
await removeSecure(StorageKeys.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储键常量
|
||||
class StorageKeys {
|
||||
// 用户相关
|
||||
static const String accessToken = 'access_token';
|
||||
static const String refreshToken = 'refresh_token';
|
||||
static const String userInfo = 'user_info';
|
||||
static const String isLoggedIn = 'is_logged_in';
|
||||
static const String rememberMe = 'remember_me';
|
||||
|
||||
// 应用设置
|
||||
static const String appLanguage = 'app_language';
|
||||
static const String appTheme = 'app_theme';
|
||||
static const String firstLaunch = 'first_launch';
|
||||
static const String onboardingCompleted = 'onboarding_completed';
|
||||
|
||||
// 学习设置
|
||||
static const String dailyGoal = 'daily_goal';
|
||||
static const String reminderTimes = 'reminder_times';
|
||||
static const String notificationsEnabled = 'notifications_enabled';
|
||||
static const String soundEnabled = 'sound_enabled';
|
||||
static const String vibrationEnabled = 'vibration_enabled';
|
||||
|
||||
// 学习数据
|
||||
static const String learningProgress = 'learning_progress';
|
||||
static const String vocabularyProgress = 'vocabulary_progress';
|
||||
static const String listeningProgress = 'listening_progress';
|
||||
static const String readingProgress = 'reading_progress';
|
||||
static const String writingProgress = 'writing_progress';
|
||||
static const String speakingProgress = 'speaking_progress';
|
||||
|
||||
// 缓存数据
|
||||
static const String cachedWordBooks = 'cached_word_books';
|
||||
static const String cachedArticles = 'cached_articles';
|
||||
static const String cachedExercises = 'cached_exercises';
|
||||
|
||||
// 临时数据
|
||||
static const String tempData = 'temp_data';
|
||||
static const String draftData = 'draft_data';
|
||||
}
|
||||
151
client/lib/core/services/tts_service.dart
Normal file
151
client/lib/core/services/tts_service.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
|
||||
/// TTS语音播报服务
|
||||
class TTSService {
|
||||
static final TTSService _instance = TTSService._internal();
|
||||
factory TTSService() => _instance;
|
||||
TTSService._internal();
|
||||
|
||||
final FlutterTts _flutterTts = FlutterTts();
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// 初始化TTS
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
try {
|
||||
// 设置语言为美式英语
|
||||
await _flutterTts.setLanguage("en-US");
|
||||
|
||||
// 设置语速 (0.0 - 1.0)
|
||||
await _flutterTts.setSpeechRate(0.5);
|
||||
|
||||
// 设置音量 (0.0 - 1.0)
|
||||
await _flutterTts.setVolume(1.0);
|
||||
|
||||
// 设置音调 (0.5 - 2.0)
|
||||
await _flutterTts.setPitch(1.0);
|
||||
|
||||
_isInitialized = true;
|
||||
print('TTS服务初始化成功');
|
||||
} catch (e) {
|
||||
print('TTS服务初始化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放单词发音
|
||||
/// [word] 要播放的单词
|
||||
/// [language] 语言代码,默认为美式英语 "en-US",英式英语为 "en-GB"
|
||||
Future<void> speak(String word, {String language = "en-US"}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果正在播放,先停止
|
||||
await _flutterTts.stop();
|
||||
|
||||
// 设置语言
|
||||
await _flutterTts.setLanguage(language);
|
||||
|
||||
// 播放单词
|
||||
await _flutterTts.speak(word);
|
||||
|
||||
print('播放单词发音: $word ($language)');
|
||||
} catch (e) {
|
||||
print('播放单词发音失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止播放
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
await _flutterTts.stop();
|
||||
} catch (e) {
|
||||
print('停止播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停播放
|
||||
Future<void> pause() async {
|
||||
try {
|
||||
await _flutterTts.pause();
|
||||
} catch (e) {
|
||||
print('暂停播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置语速
|
||||
/// [rate] 语速 (0.0 - 1.0)
|
||||
Future<void> setSpeechRate(double rate) async {
|
||||
try {
|
||||
await _flutterTts.setSpeechRate(rate);
|
||||
} catch (e) {
|
||||
print('设置语速失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置音调
|
||||
/// [pitch] 音调 (0.5 - 2.0)
|
||||
Future<void> setPitch(double pitch) async {
|
||||
try {
|
||||
await _flutterTts.setPitch(pitch);
|
||||
} catch (e) {
|
||||
print('设置音调失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置音量
|
||||
/// [volume] 音量 (0.0 - 1.0)
|
||||
Future<void> setVolume(double volume) async {
|
||||
try {
|
||||
await _flutterTts.setVolume(volume);
|
||||
} catch (e) {
|
||||
print('设置音量失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取可用语言列表
|
||||
Future<List<dynamic>> getLanguages() async {
|
||||
try {
|
||||
return await _flutterTts.getLanguages;
|
||||
} catch (e) {
|
||||
print('获取语言列表失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取可用语音列表
|
||||
Future<List<dynamic>> getVoices() async {
|
||||
try {
|
||||
return await _flutterTts.getVoices;
|
||||
} catch (e) {
|
||||
print('获取语音列表失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置完成回调
|
||||
void setCompletionHandler(Function callback) {
|
||||
_flutterTts.setCompletionHandler(() {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
/// 设置错误回调
|
||||
void setErrorHandler(Function(dynamic) callback) {
|
||||
_flutterTts.setErrorHandler((msg) {
|
||||
callback(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
Future<void> dispose() async {
|
||||
try {
|
||||
await _flutterTts.stop();
|
||||
_isInitialized = false;
|
||||
} catch (e) {
|
||||
print('释放TTS资源失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user