This commit is contained in:
sjk
2025-11-17 14:09:17 +08:00
commit 31e46c5bf6
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
/// 环境配置
enum Environment {
development,
staging,
production,
}
/// 环境配置管理
class EnvironmentConfig {
static Environment _currentEnvironment = Environment.development;
/// 获取当前环境
static Environment get current => _currentEnvironment;
/// 设置当前环境
static void setEnvironment(Environment env) {
_currentEnvironment = env;
}
/// 从字符串设置环境
static void setEnvironmentFromString(String? envString) {
switch (envString?.toLowerCase()) {
case 'production':
case 'prod':
_currentEnvironment = Environment.production;
break;
case 'staging':
case 'stage':
_currentEnvironment = Environment.staging;
break;
case 'development':
case 'dev':
default:
_currentEnvironment = Environment.development;
break;
}
}
/// 获取当前环境的API基础URL
static String get baseUrl {
switch (_currentEnvironment) {
case Environment.production:
return 'https://loukao.cn/api/v1';
case Environment.staging:
return 'http://localhost:8080/api/v1';
case Environment.development:
default:
// 开发环境localhost 用于 Web10.0.2.2 用于 Android 模拟器
return const String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080/api/v1',
);
}
}
/// 获取环境名称
static String get environmentName {
switch (_currentEnvironment) {
case Environment.production:
return 'Production';
case Environment.staging:
return 'Staging';
case Environment.development:
return 'Development';
}
}
/// 是否为开发环境
static bool get isDevelopment => _currentEnvironment == Environment.development;
/// 是否为生产环境
static bool get isProduction => _currentEnvironment == Environment.production;
/// 是否为预发布环境
static bool get isStaging => _currentEnvironment == Environment.staging;
/// 开发环境配置
static const Map<String, String> developmentConfig = {
'baseUrl': 'http://localhost:8080/api/v1',
'baseUrlAndroid': 'http://10.0.2.2:8080/api/v1',
'wsUrl': 'ws://localhost:8080/ws',
};
/// 预发布环境配置
static const Map<String, String> stagingConfig = {
'baseUrl': 'https://loukao.cn/api/v1',
'wsUrl': 'ws://your-staging-domain.com/ws',
};
/// 生产环境配置
static const Map<String, String> productionConfig = {
'baseUrl': 'https://loukao.cn/api/v1',
'wsUrl': 'ws://your-production-domain.com/ws',
};
/// 获取当前环境配置
static Map<String, String> get config {
switch (_currentEnvironment) {
case Environment.production:
return productionConfig;
case Environment.staging:
return stagingConfig;
case Environment.development:
default:
return developmentConfig;
}
}
/// 获取WebSocket URL
static String get wsUrl {
return config['wsUrl'] ?? '';
}
/// 获取连接超时时间(毫秒)
static int get connectTimeout {
return isProduction ? 10000 : 30000;
}
/// 获取接收超时时间(毫秒)
static int get receiveTimeout {
return isProduction ? 10000 : 30000;
}
/// 是否启用日志
static bool get enableLogging {
return !isProduction;
}
/// 是否启用调试模式
static bool get debugMode {
return isDevelopment;
}
}

View File

@@ -0,0 +1,88 @@
import '../config/environment.dart';
/// 应用常量配置
class AppConstants {
// 应用信息
static const String appName = 'AI英语学习';
static const String appVersion = '1.0.0';
// API配置 - 从环境配置获取
static String get baseUrl => EnvironmentConfig.baseUrl;
static int get connectTimeout => EnvironmentConfig.connectTimeout;
static int get receiveTimeout => EnvironmentConfig.receiveTimeout;
// 存储键名
static const String accessTokenKey = 'access_token';
static const String refreshTokenKey = 'refresh_token';
static const String userInfoKey = 'user_info';
static const String settingsKey = 'app_settings';
// 分页配置
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// 学习配置
static const int dailyWordGoal = 50;
static const int maxRetryAttempts = 3;
static const Duration studySessionDuration = Duration(minutes: 25);
// 音频配置
static const double defaultPlaybackSpeed = 1.0;
static const double minPlaybackSpeed = 0.5;
static const double maxPlaybackSpeed = 2.0;
// 图片配置
static const int maxImageSize = 5 * 1024 * 1024; // 5MB
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
// 缓存配置
static const Duration cacheExpiration = Duration(hours: 24);
static const int maxCacheSize = 100 * 1024 * 1024; // 100MB
}
/// 路由常量
class RouteConstants {
static const String splash = '/splash';
static const String onboarding = '/onboarding';
static const String login = '/login';
static const String register = '/register';
static const String home = '/home';
static const String profile = '/profile';
static const String vocabulary = '/vocabulary';
static const String vocabularyTest = '/vocabulary/test';
static const String listening = '/listening';
static const String reading = '/reading';
static const String writing = '/writing';
static const String speaking = '/speaking';
static const String settings = '/settings';
}
/// 学习等级常量
enum LearningLevel {
beginner('beginner', '初级'),
intermediate('intermediate', '中级'),
advanced('advanced', '高级');
const LearningLevel(this.value, this.label);
final String value;
final String label;
}
/// 词库类型常量
enum VocabularyType {
elementary('elementary', '小学'),
junior('junior', '初中'),
senior('senior', '高中'),
cet4('cet4', '四级'),
cet6('cet6', '六级'),
toefl('toefl', '托福'),
ielts('ielts', '雅思'),
business('business', '商务'),
daily('daily', '日常');
const VocabularyType(this.value, this.label);
final String value;
final String label;
}

View File

@@ -0,0 +1,299 @@
/// 应用错误基类
abstract class AppError implements Exception {
final String message;
final String? code;
final dynamic originalError;
const AppError({
required this.message,
this.code,
this.originalError,
});
@override
String toString() {
return 'AppError(message: $message, code: $code)';
}
}
/// 网络错误
class NetworkError extends AppError {
const NetworkError({
required super.message,
super.code,
super.originalError,
});
factory NetworkError.connectionTimeout() {
return const NetworkError(
message: '连接超时,请检查网络连接',
code: 'CONNECTION_TIMEOUT',
);
}
factory NetworkError.noInternet() {
return const NetworkError(
message: '网络连接不可用,请检查网络设置',
code: 'NO_INTERNET',
);
}
factory NetworkError.serverError(int statusCode, [String? message]) {
return NetworkError(
message: message ?? '服务器错误 ($statusCode)',
code: 'SERVER_ERROR_$statusCode',
);
}
factory NetworkError.unknown([dynamic error]) {
return NetworkError(
message: '网络请求失败',
code: 'UNKNOWN_NETWORK_ERROR',
originalError: error,
);
}
}
/// 认证错误
class AuthError extends AppError {
const AuthError({
required super.message,
super.code,
super.originalError,
});
factory AuthError.unauthorized() {
return const AuthError(
message: '未授权访问,请重新登录',
code: 'UNAUTHORIZED',
);
}
factory AuthError.tokenExpired() {
return const AuthError(
message: '登录已过期,请重新登录',
code: 'TOKEN_EXPIRED',
);
}
factory AuthError.invalidCredentials() {
return const AuthError(
message: '用户名或密码错误',
code: 'INVALID_CREDENTIALS',
);
}
factory AuthError.accountLocked() {
return const AuthError(
message: '账户已被锁定,请联系客服',
code: 'ACCOUNT_LOCKED',
);
}
}
/// 验证错误
class ValidationError extends AppError {
final Map<String, List<String>>? fieldErrors;
const ValidationError({
required super.message,
super.code,
super.originalError,
this.fieldErrors,
});
factory ValidationError.required(String field) {
return ValidationError(
message: '$field不能为空',
code: 'FIELD_REQUIRED',
fieldErrors: {field: ['不能为空']},
);
}
factory ValidationError.invalid(String field, String reason) {
return ValidationError(
message: '$field格式不正确$reason',
code: 'FIELD_INVALID',
fieldErrors: {field: [reason]},
);
}
factory ValidationError.multiple(Map<String, List<String>> errors) {
return ValidationError(
message: '表单验证失败',
code: 'VALIDATION_FAILED',
fieldErrors: errors,
);
}
}
/// 业务逻辑错误
class BusinessError extends AppError {
const BusinessError({
required super.message,
super.code,
super.originalError,
});
factory BusinessError.notFound(String resource) {
return BusinessError(
message: '$resource不存在',
code: 'RESOURCE_NOT_FOUND',
);
}
factory BusinessError.alreadyExists(String resource) {
return BusinessError(
message: '$resource已存在',
code: 'RESOURCE_ALREADY_EXISTS',
);
}
factory BusinessError.operationNotAllowed(String operation) {
return BusinessError(
message: '不允许执行操作:$operation',
code: 'OPERATION_NOT_ALLOWED',
);
}
factory BusinessError.quotaExceeded(String resource) {
return BusinessError(
message: '$resource配额已用完',
code: 'QUOTA_EXCEEDED',
);
}
}
/// 存储错误
class StorageError extends AppError {
const StorageError({
required super.message,
super.code,
super.originalError,
});
factory StorageError.readFailed(String key) {
return StorageError(
message: '读取数据失败:$key',
code: 'STORAGE_READ_FAILED',
);
}
factory StorageError.writeFailed(String key) {
return StorageError(
message: '写入数据失败:$key',
code: 'STORAGE_WRITE_FAILED',
);
}
factory StorageError.notInitialized() {
return const StorageError(
message: '存储服务未初始化',
code: 'STORAGE_NOT_INITIALIZED',
);
}
}
/// 文件错误
class FileError extends AppError {
const FileError({
required super.message,
super.code,
super.originalError,
});
factory FileError.notFound(String path) {
return FileError(
message: '文件不存在:$path',
code: 'FILE_NOT_FOUND',
);
}
factory FileError.accessDenied(String path) {
return FileError(
message: '文件访问被拒绝:$path',
code: 'FILE_ACCESS_DENIED',
);
}
factory FileError.formatNotSupported(String format) {
return FileError(
message: '不支持的文件格式:$format',
code: 'FILE_FORMAT_NOT_SUPPORTED',
);
}
factory FileError.sizeTooLarge(int size, int maxSize) {
return FileError(
message: '文件大小超出限制:${size}B > ${maxSize}B',
code: 'FILE_SIZE_TOO_LARGE',
);
}
}
/// 音频错误
class AudioError extends AppError {
const AudioError({
required super.message,
super.code,
super.originalError,
});
factory AudioError.playbackFailed() {
return const AudioError(
message: '音频播放失败',
code: 'AUDIO_PLAYBACK_FAILED',
);
}
factory AudioError.recordingFailed() {
return const AudioError(
message: '音频录制失败',
code: 'AUDIO_RECORDING_FAILED',
);
}
factory AudioError.permissionDenied() {
return const AudioError(
message: '音频权限被拒绝',
code: 'AUDIO_PERMISSION_DENIED',
);
}
}
/// 学习相关错误
class LearningError extends AppError {
const LearningError({
required super.message,
super.code,
super.originalError,
});
factory LearningError.progressNotFound() {
return const LearningError(
message: '学习进度不存在',
code: 'LEARNING_PROGRESS_NOT_FOUND',
);
}
factory LearningError.vocabularyNotFound() {
return const LearningError(
message: '词汇不存在',
code: 'VOCABULARY_NOT_FOUND',
);
}
factory LearningError.testNotCompleted() {
return const LearningError(
message: '测试未完成',
code: 'TEST_NOT_COMPLETED',
);
}
factory LearningError.levelNotUnlocked() {
return const LearningError(
message: '等级未解锁',
code: 'LEVEL_NOT_UNLOCKED',
);
}
}

View File

@@ -0,0 +1,62 @@
/// 应用异常基类
class AppException implements Exception {
final String message;
final String? code;
final dynamic details;
const AppException(
this.message, {
this.code,
this.details,
});
@override
String toString() {
return 'AppException: $message';
}
}
/// 网络异常
class NetworkException extends AppException {
const NetworkException(
super.message, {
super.code,
super.details,
});
}
/// 认证异常
class AuthException extends AppException {
const AuthException(
super.message, {
super.code,
super.details,
});
}
/// 服务器异常
class ServerException extends AppException {
const ServerException(
super.message, {
super.code,
super.details,
});
}
/// 缓存异常
class CacheException extends AppException {
const CacheException(
super.message, {
super.code,
super.details,
});
}
/// 验证异常
class ValidationException extends AppException {
const ValidationException(
super.message, {
super.code,
super.details,
});
}

View File

@@ -0,0 +1,121 @@
/// API响应基础模型
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final int? code;
final Map<String, dynamic>? errors;
const ApiResponse({
required this.success,
required this.message,
this.data,
this.code,
this.errors,
});
factory ApiResponse.success({
required String message,
T? data,
int? code,
}) {
return ApiResponse<T>(
success: true,
message: message,
data: data,
code: code ?? 200,
);
}
factory ApiResponse.error({
required String message,
int? code,
Map<String, dynamic>? errors,
}) {
return ApiResponse<T>(
success: false,
message: message,
code: code ?? 400,
errors: errors,
);
}
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse<T>(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null && fromJsonT != null
? fromJsonT(json['data'])
: json['data'],
code: json['code'],
errors: json['errors'],
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data,
'code': code,
'errors': errors,
};
}
@override
String toString() {
return 'ApiResponse{success: $success, message: $message, data: $data, code: $code}';
}
}
/// 分页响应模型
class PaginatedResponse<T> {
final List<T> data;
final int total;
final int page;
final int pageSize;
final int totalPages;
final bool hasNext;
final bool hasPrevious;
const PaginatedResponse({
required this.data,
required this.total,
required this.page,
required this.pageSize,
required this.totalPages,
required this.hasNext,
required this.hasPrevious,
});
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
final List<dynamic> dataList = json['data'] ?? [];
return PaginatedResponse<T>(
data: dataList.map((item) => fromJsonT(item)).toList(),
total: json['total'] ?? 0,
page: json['page'] ?? 1,
pageSize: json['page_size'] ?? 10,
totalPages: json['total_pages'] ?? 0,
hasNext: json['has_next'] ?? false,
hasPrevious: json['has_previous'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'data': data,
'total': total,
'page': page,
'page_size': pageSize,
'total_pages': totalPages,
'has_next': hasNext,
'has_previous': hasPrevious,
};
}
}

View File

@@ -0,0 +1,280 @@
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
/// 用户模型
@JsonSerializable()
class User {
final String id;
final String username;
final String email;
final String? phone;
final String? avatar;
final DateTime createdAt;
final DateTime updatedAt;
final UserProfile? profile;
final UserSettings? settings;
const User({
required this.id,
required this.username,
required this.email,
this.phone,
this.avatar,
required this.createdAt,
required this.updatedAt,
this.profile,
this.settings,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
User copyWith({
String? id,
String? username,
String? email,
String? phone,
String? avatar,
DateTime? createdAt,
DateTime? updatedAt,
UserProfile? profile,
UserSettings? settings,
}) {
return User(
id: id ?? this.id,
username: username ?? this.username,
email: email ?? this.email,
phone: phone ?? this.phone,
avatar: avatar ?? this.avatar,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
profile: profile ?? this.profile,
settings: settings ?? this.settings,
);
}
}
/// 用户资料
@JsonSerializable()
class UserProfile {
final String? firstName;
final String? lastName;
final String? phone;
final String? bio;
final String? avatar;
final String? realName;
final String? gender;
final DateTime? birthday;
final String? location;
final String? occupation;
final String? education;
final List<String>? interests;
final LearningGoal? learningGoal;
final EnglishLevel? currentLevel;
final EnglishLevel? targetLevel;
final EnglishLevel? englishLevel;
final UserSettings? settings;
const UserProfile({
this.firstName,
this.lastName,
this.phone,
this.bio,
this.avatar,
this.realName,
this.gender,
this.birthday,
this.location,
this.occupation,
this.education,
this.interests,
this.learningGoal,
this.currentLevel,
this.targetLevel,
this.englishLevel,
this.settings,
});
factory UserProfile.fromJson(Map<String, dynamic> json) => _$UserProfileFromJson(json);
Map<String, dynamic> toJson() => _$UserProfileToJson(this);
UserProfile copyWith({
String? firstName,
String? lastName,
String? phone,
String? bio,
String? avatar,
String? realName,
String? gender,
DateTime? birthday,
String? location,
String? occupation,
String? education,
List<String>? interests,
LearningGoal? learningGoal,
EnglishLevel? currentLevel,
EnglishLevel? targetLevel,
EnglishLevel? englishLevel,
UserSettings? settings,
}) {
return UserProfile(
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
phone: phone ?? this.phone,
bio: bio ?? this.bio,
avatar: avatar ?? this.avatar,
realName: realName ?? this.realName,
gender: gender ?? this.gender,
birthday: birthday ?? this.birthday,
location: location ?? this.location,
occupation: occupation ?? this.occupation,
education: education ?? this.education,
interests: interests ?? this.interests,
learningGoal: learningGoal ?? this.learningGoal,
currentLevel: currentLevel ?? this.currentLevel,
targetLevel: targetLevel ?? this.targetLevel,
englishLevel: englishLevel ?? this.englishLevel,
settings: settings ?? this.settings,
);
}
}
/// 用户设置
@JsonSerializable()
class UserSettings {
final bool notificationsEnabled;
final bool soundEnabled;
final bool vibrationEnabled;
final String language;
final String theme;
final int dailyGoal;
final int dailyWordGoal;
final int dailyStudyMinutes;
final List<String> reminderTimes;
final bool autoPlayAudio;
final double audioSpeed;
final bool showTranslation;
final bool showPronunciation;
const UserSettings({
this.notificationsEnabled = true,
this.soundEnabled = true,
this.vibrationEnabled = true,
this.language = 'zh-CN',
this.theme = 'system',
this.dailyGoal = 30,
this.dailyWordGoal = 20,
this.dailyStudyMinutes = 30,
this.reminderTimes = const ['09:00', '20:00'],
this.autoPlayAudio = true,
this.audioSpeed = 1.0,
this.showTranslation = true,
this.showPronunciation = true,
});
factory UserSettings.fromJson(Map<String, dynamic> json) => _$UserSettingsFromJson(json);
Map<String, dynamic> toJson() => _$UserSettingsToJson(this);
UserSettings copyWith({
bool? notificationsEnabled,
bool? soundEnabled,
bool? vibrationEnabled,
String? language,
String? theme,
int? dailyGoal,
int? dailyWordGoal,
int? dailyStudyMinutes,
List<String>? reminderTimes,
bool? autoPlayAudio,
double? audioSpeed,
bool? showTranslation,
bool? showPronunciation,
}) {
return UserSettings(
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
soundEnabled: soundEnabled ?? this.soundEnabled,
vibrationEnabled: vibrationEnabled ?? this.vibrationEnabled,
language: language ?? this.language,
theme: theme ?? this.theme,
dailyGoal: dailyGoal ?? this.dailyGoal,
dailyWordGoal: dailyWordGoal ?? this.dailyWordGoal,
dailyStudyMinutes: dailyStudyMinutes ?? this.dailyStudyMinutes,
reminderTimes: reminderTimes ?? this.reminderTimes,
autoPlayAudio: autoPlayAudio ?? this.autoPlayAudio,
audioSpeed: audioSpeed ?? this.audioSpeed,
showTranslation: showTranslation ?? this.showTranslation,
showPronunciation: showPronunciation ?? this.showPronunciation,
);
}
}
/// 学习目标
enum LearningGoal {
@JsonValue('daily_communication')
dailyCommunication,
@JsonValue('business_english')
businessEnglish,
@JsonValue('academic_study')
academicStudy,
@JsonValue('exam_preparation')
examPreparation,
@JsonValue('travel')
travel,
@JsonValue('hobby')
hobby,
}
/// 英语水平
enum EnglishLevel {
@JsonValue('beginner')
beginner,
@JsonValue('elementary')
elementary,
@JsonValue('intermediate')
intermediate,
@JsonValue('upper_intermediate')
upperIntermediate,
@JsonValue('advanced')
advanced,
@JsonValue('proficient')
proficient,
@JsonValue('expert')
expert,
}
/// 认证响应
@JsonSerializable()
class AuthResponse {
final User user;
final String token;
final String? refreshToken;
final DateTime expiresAt;
const AuthResponse({
required this.user,
required this.token,
this.refreshToken,
required this.expiresAt,
});
factory AuthResponse.fromJson(Map<String, dynamic> json) => _$AuthResponseFromJson(json);
Map<String, dynamic> toJson() => _$AuthResponseToJson(this);
}
/// Token刷新响应
@JsonSerializable()
class TokenRefreshResponse {
final String token;
final String? refreshToken;
final DateTime expiresAt;
const TokenRefreshResponse({
required this.token,
this.refreshToken,
required this.expiresAt,
});
factory TokenRefreshResponse.fromJson(Map<String, dynamic> json) => _$TokenRefreshResponseFromJson(json);
Map<String, dynamic> toJson() => _$TokenRefreshResponseToJson(this);
}

View File

@@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
username: json['username'] as String,
email: json['email'] as String,
phone: json['phone'] as String?,
avatar: json['avatar'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
profile: json['profile'] == null
? null
: UserProfile.fromJson(json['profile'] as Map<String, dynamic>),
settings: json['settings'] == null
? null
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'username': instance.username,
'email': instance.email,
'phone': instance.phone,
'avatar': instance.avatar,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
'profile': instance.profile,
'settings': instance.settings,
};
UserProfile _$UserProfileFromJson(Map<String, dynamic> json) => UserProfile(
firstName: json['firstName'] as String?,
lastName: json['lastName'] as String?,
phone: json['phone'] as String?,
bio: json['bio'] as String?,
avatar: json['avatar'] as String?,
realName: json['realName'] as String?,
gender: json['gender'] as String?,
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
location: json['location'] as String?,
occupation: json['occupation'] as String?,
education: json['education'] as String?,
interests: (json['interests'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
learningGoal:
$enumDecodeNullable(_$LearningGoalEnumMap, json['learningGoal']),
currentLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['currentLevel']),
targetLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['targetLevel']),
englishLevel:
$enumDecodeNullable(_$EnglishLevelEnumMap, json['englishLevel']),
settings: json['settings'] == null
? null
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$UserProfileToJson(UserProfile instance) =>
<String, dynamic>{
'firstName': instance.firstName,
'lastName': instance.lastName,
'phone': instance.phone,
'bio': instance.bio,
'avatar': instance.avatar,
'realName': instance.realName,
'gender': instance.gender,
'birthday': instance.birthday?.toIso8601String(),
'location': instance.location,
'occupation': instance.occupation,
'education': instance.education,
'interests': instance.interests,
'learningGoal': _$LearningGoalEnumMap[instance.learningGoal],
'currentLevel': _$EnglishLevelEnumMap[instance.currentLevel],
'targetLevel': _$EnglishLevelEnumMap[instance.targetLevel],
'englishLevel': _$EnglishLevelEnumMap[instance.englishLevel],
'settings': instance.settings,
};
const _$LearningGoalEnumMap = {
LearningGoal.dailyCommunication: 'daily_communication',
LearningGoal.businessEnglish: 'business_english',
LearningGoal.academicStudy: 'academic_study',
LearningGoal.examPreparation: 'exam_preparation',
LearningGoal.travel: 'travel',
LearningGoal.hobby: 'hobby',
};
const _$EnglishLevelEnumMap = {
EnglishLevel.beginner: 'beginner',
EnglishLevel.elementary: 'elementary',
EnglishLevel.intermediate: 'intermediate',
EnglishLevel.upperIntermediate: 'upper_intermediate',
EnglishLevel.advanced: 'advanced',
EnglishLevel.proficient: 'proficient',
EnglishLevel.expert: 'expert',
};
UserSettings _$UserSettingsFromJson(Map<String, dynamic> json) => UserSettings(
notificationsEnabled: json['notificationsEnabled'] as bool? ?? true,
soundEnabled: json['soundEnabled'] as bool? ?? true,
vibrationEnabled: json['vibrationEnabled'] as bool? ?? true,
language: json['language'] as String? ?? 'zh-CN',
theme: json['theme'] as String? ?? 'system',
dailyGoal: (json['dailyGoal'] as num?)?.toInt() ?? 30,
dailyWordGoal: (json['dailyWordGoal'] as num?)?.toInt() ?? 20,
dailyStudyMinutes: (json['dailyStudyMinutes'] as num?)?.toInt() ?? 30,
reminderTimes: (json['reminderTimes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['09:00', '20:00'],
autoPlayAudio: json['autoPlayAudio'] as bool? ?? true,
audioSpeed: (json['audioSpeed'] as num?)?.toDouble() ?? 1.0,
showTranslation: json['showTranslation'] as bool? ?? true,
showPronunciation: json['showPronunciation'] as bool? ?? true,
);
Map<String, dynamic> _$UserSettingsToJson(UserSettings instance) =>
<String, dynamic>{
'notificationsEnabled': instance.notificationsEnabled,
'soundEnabled': instance.soundEnabled,
'vibrationEnabled': instance.vibrationEnabled,
'language': instance.language,
'theme': instance.theme,
'dailyGoal': instance.dailyGoal,
'dailyWordGoal': instance.dailyWordGoal,
'dailyStudyMinutes': instance.dailyStudyMinutes,
'reminderTimes': instance.reminderTimes,
'autoPlayAudio': instance.autoPlayAudio,
'audioSpeed': instance.audioSpeed,
'showTranslation': instance.showTranslation,
'showPronunciation': instance.showPronunciation,
};
AuthResponse _$AuthResponseFromJson(Map<String, dynamic> json) => AuthResponse(
user: User.fromJson(json['user'] as Map<String, dynamic>),
token: json['token'] as String,
refreshToken: json['refreshToken'] as String?,
expiresAt: DateTime.parse(json['expiresAt'] as String),
);
Map<String, dynamic> _$AuthResponseToJson(AuthResponse instance) =>
<String, dynamic>{
'user': instance.user,
'token': instance.token,
'refreshToken': instance.refreshToken,
'expiresAt': instance.expiresAt.toIso8601String(),
};
TokenRefreshResponse _$TokenRefreshResponseFromJson(
Map<String, dynamic> json) =>
TokenRefreshResponse(
token: json['token'] as String,
refreshToken: json['refreshToken'] as String?,
expiresAt: DateTime.parse(json['expiresAt'] as String),
);
Map<String, dynamic> _$TokenRefreshResponseToJson(
TokenRefreshResponse instance) =>
<String, dynamic>{
'token': instance.token,
'refreshToken': instance.refreshToken,
'expiresAt': instance.expiresAt.toIso8601String(),
};

View File

@@ -0,0 +1,209 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../services/storage_service.dart';
import 'api_endpoints.dart';
import '../config/environment.dart';
/// AI相关API服务
class AIApiService {
static String get _baseUrl => EnvironmentConfig.baseUrl;
/// 获取认证头部
Map<String, String> _getAuthHeaders() {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
return {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
/// 写作批改
Future<Map<String, dynamic>> correctWriting({
required String content,
required String taskType,
}) async {
try {
final headers = _getAuthHeaders();
final response = await http.post(
Uri.parse('$_baseUrl/api/v1/ai/writing/correct'),
headers: headers,
body: json.encode({
'content': content,
'task_type': taskType,
}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to correct writing: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error correcting writing: $e');
}
}
/// 口语评估
Future<Map<String, dynamic>> evaluateSpeaking({
required String audioText,
required String prompt,
}) async {
try {
final headers = _getAuthHeaders();
final response = await http.post(
Uri.parse('$_baseUrl/api/v1/ai/speaking/evaluate'),
headers: headers,
body: json.encode({
'audio_text': audioText,
'prompt': prompt,
}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to evaluate speaking: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error evaluating speaking: $e');
}
}
/// 获取AI使用统计
Future<Map<String, dynamic>> getAIUsageStats() async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/ai/stats'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get AI stats: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting AI stats: $e');
}
}
/// 上传音频文件
Future<Map<String, dynamic>> uploadAudio(File audioFile) async {
try {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
final request = http.MultipartRequest(
'POST',
Uri.parse('$_baseUrl/api/v1/upload/audio'),
);
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.files.add(
await http.MultipartFile.fromPath('audio', audioFile.path),
);
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to upload audio: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error uploading audio: $e');
}
}
/// 上传图片文件
Future<Map<String, dynamic>> uploadImage(File imageFile) async {
try {
final storageService = StorageService.instance;
final token = storageService.getString(StorageKeys.accessToken);
final request = http.MultipartRequest(
'POST',
Uri.parse('$_baseUrl/api/v1/upload/image'),
);
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.files.add(
await http.MultipartFile.fromPath('image', imageFile.path),
);
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to upload image: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error uploading image: $e');
}
}
/// 删除文件
Future<Map<String, dynamic>> deleteFile(String fileId) async {
try {
final headers = _getAuthHeaders();
final response = await http.delete(
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to delete file: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error deleting file: $e');
}
}
/// 获取文件信息
Future<Map<String, dynamic>> getFileInfo(String fileId) async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get file info: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting file info: $e');
}
}
/// 获取上传统计
Future<Map<String, dynamic>> getUploadStats({int days = 30}) async {
try {
final headers = _getAuthHeaders();
final response = await http.get(
Uri.parse('$_baseUrl/api/v1/upload/stats?days=$days'),
headers: headers,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to get upload stats: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting upload stats: $e');
}
}
}

View File

@@ -0,0 +1,252 @@
import 'package:dio/dio.dart';
import '../constants/app_constants.dart';
import '../services/storage_service.dart';
import '../services/navigation_service.dart';
import '../routes/app_routes.dart';
/// API客户端配置
class ApiClient {
static ApiClient? _instance;
late Dio _dio;
late StorageService _storageService;
ApiClient._internal() {
_dio = Dio();
}
static Future<ApiClient> getInstance() async {
if (_instance == null) {
_instance = ApiClient._internal();
_instance!._storageService = await StorageService.getInstance();
await _instance!._setupInterceptors();
}
return _instance!;
}
static ApiClient get instance {
if (_instance == null) {
throw Exception('ApiClient not initialized. Call ApiClient.getInstance() first.');
}
return _instance!;
}
Dio get dio => _dio;
/// 配置拦截器
Future<void> _setupInterceptors() async {
// 基础配置
_dio.options = BaseOptions(
baseUrl: AppConstants.baseUrl,
connectTimeout: Duration(milliseconds: AppConstants.connectTimeout),
receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeout),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
// 请求拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
// 添加认证token
final token = await _storageService.getToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onResponse: (response, handler) {
handler.next(response);
},
onError: (error, handler) async {
// 处理401错误尝试刷新token
if (error.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// 重新发送请求
final options = error.requestOptions;
final token = await _storageService.getToken();
options.headers['Authorization'] = 'Bearer $token';
try {
final response = await _dio.fetch(options);
handler.resolve(response);
return;
} catch (e) {
// 刷新后仍然失败清除token并跳转登录
await _clearTokensAndRedirectToLogin();
}
} else {
// 刷新失败清除token并跳转登录
await _clearTokensAndRedirectToLogin();
}
}
handler.next(error);
},
),
);
// 日志拦截器(仅在调试模式下)
if (const bool.fromEnvironment('dart.vm.product') == false) {
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
logPrint: (obj) => print(obj),
),
);
}
}
/// 刷新token
Future<bool> _refreshToken() async {
try {
final refreshToken = await _storageService.getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty) {
return false;
}
final response = await _dio.post(
'/auth/refresh',
data: {'refresh_token': refreshToken},
options: Options(
headers: {'Authorization': null}, // 移除Authorization头
),
);
if (response.statusCode == 200) {
final data = response.data;
await _storageService.saveToken(data['access_token']);
if (data['refresh_token'] != null) {
await _storageService.saveRefreshToken(data['refresh_token']);
}
return true;
}
} catch (e) {
print('Token refresh failed: $e');
}
return false;
}
/// 清除token并跳转登录
Future<void> _clearTokensAndRedirectToLogin() async {
await _storageService.clearTokens();
// 跳转到登录页面并清除所有历史记录
NavigationService.instance.navigateToAndClearStack(Routes.login);
// 显示提示信息
NavigationService.instance.showErrorSnackBar('登录已过期,请重新登录');
}
/// GET请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// POST请求
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// PUT请求
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// DELETE请求
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
/// 上传文件
Future<Response<T>> upload<T>(
String path,
FormData formData, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
return await _dio.post<T>(
path,
data: formData,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
}
/// 下载文件
Future<Response> download(
String urlPath,
String savePath, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.download(
urlPath,
savePath,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
}

View File

@@ -0,0 +1,77 @@
import '../config/environment.dart';
/// API端点配置
class ApiEndpoints {
// 基础URL - 从环境配置获取
static String get baseUrl => EnvironmentConfig.baseUrl;
// 认证相关
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String logout = '/auth/logout';
static const String refreshToken = '/auth/refresh';
static const String forgotPassword = '/auth/forgot-password';
static const String resetPassword = '/auth/reset-password';
static const String changePassword = '/auth/change-password';
static const String socialLogin = '/auth/social-login';
static const String verifyEmail = '/auth/verify-email';
static const String resendVerificationEmail = '/auth/resend-verification';
// 用户相关
static const String userInfo = '/user/profile';
static const String updateProfile = '/user/profile';
static const String uploadAvatar = '/user/avatar';
static const String checkUsername = '/user/check-username';
static const String checkEmail = '/user/check-email';
// 学习相关
static const String learningProgress = '/learning/progress';
static const String learningStats = '/learning/stats';
static const String dailyGoal = '/learning/daily-goal';
// 词汇相关
static const String vocabulary = '/vocabulary';
static const String vocabularyTest = '/vocabulary/test';
static const String vocabularyProgress = '/vocabulary/progress';
static const String wordBooks = '/vocabulary/books';
static const String wordLists = '/vocabulary/lists';
// 听力相关
static const String listening = '/listening';
static const String listeningMaterials = '/listening/materials';
static const String listeningRecords = '/listening/records';
static const String listeningStats = '/listening/stats';
// 阅读相关
static const String reading = '/reading';
static const String readingMaterials = '/reading/materials';
static const String readingRecords = '/reading/records';
static const String readingStats = '/reading/stats';
// 写作相关
static const String writing = '/writing';
static const String writingPrompts = '/writing/prompts';
static const String writingSubmissions = '/writing/submissions';
static const String writingStats = '/writing/stats';
// 口语相关
static const String speaking = '/speaking';
static const String speakingScenarios = '/speaking/scenarios';
static const String speakingRecords = '/speaking/records';
static const String speakingStats = '/speaking/stats';
// AI相关
static const String aiChat = '/ai/chat';
static const String aiCorrection = '/ai/correction';
static const String aiSuggestion = '/ai/suggestion';
// 文件上传
static const String upload = '/upload';
static const String uploadAudio = '/upload/audio';
static const String uploadImage = '/upload/image';
// 系统相关
static const String version = '/version';
static const String config = '/system/config';
static const String feedback = '/system/feedback';
}

View File

@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../shared/providers/auth_provider.dart';
import '../../shared/providers/vocabulary_provider.dart';
import '../../shared/services/auth_service.dart';
import '../../shared/services/vocabulary_service.dart';
import '../network/api_client.dart';
/// 全局应用状态管理
class AppStateNotifier extends StateNotifier<AppState> {
AppStateNotifier() : super(const AppState());
void updateTheme(ThemeMode themeMode) {
state = state.copyWith(themeMode: themeMode);
}
void updateLocale(String locale) {
state = state.copyWith(locale: locale);
}
void updateNetworkStatus(bool isOnline) {
state = state.copyWith(isOnline: isOnline);
}
void updateLoading(bool isLoading) {
state = state.copyWith(isGlobalLoading: isLoading);
}
}
/// 应用状态模型
class AppState {
final ThemeMode themeMode;
final String locale;
final bool isOnline;
final bool isGlobalLoading;
const AppState({
this.themeMode = ThemeMode.light,
this.locale = 'zh_CN',
this.isOnline = true,
this.isGlobalLoading = false,
});
AppState copyWith({
ThemeMode? themeMode,
String? locale,
bool? isOnline,
bool? isGlobalLoading,
}) {
return AppState(
themeMode: themeMode ?? this.themeMode,
locale: locale ?? this.locale,
isOnline: isOnline ?? this.isOnline,
isGlobalLoading: isGlobalLoading ?? this.isGlobalLoading,
);
}
}
/// 全局状态Provider
final appStateProvider = StateNotifierProvider<AppStateNotifier, AppState>(
(ref) => AppStateNotifier(),
);
/// API客户端Provider
final apiClientProvider = Provider<ApiClient>(
(ref) => ApiClient.instance,
);
/// 认证服务Provider
final authServiceProvider = Provider<AuthService>(
(ref) => AuthService(),
);
/// 词汇服务Provider
final vocabularyServiceProvider = Provider<VocabularyService>(
(ref) => VocabularyService(),
);
/// 认证Provider
final authProvider = ChangeNotifierProvider<AuthProvider>(
(ref) {
final authService = ref.read(authServiceProvider);
return AuthProvider()..initialize();
},
);
/// 词汇Provider
final vocabularyProvider = ChangeNotifierProvider<VocabularyProvider>(
(ref) {
final vocabularyService = ref.read(vocabularyServiceProvider);
return VocabularyProvider(vocabularyService);
},
);
/// 网络状态Provider
final networkStatusProvider = StreamProvider<bool>(
(ref) async* {
// 这里可以实现网络状态监听
yield true; // 默认在线状态
},
);
/// 缓存管理Provider
final cacheManagerProvider = Provider<CacheManager>(
(ref) => CacheManager(),
);
/// 缓存管理器
class CacheManager {
final Map<String, dynamic> _cache = {};
final Map<String, DateTime> _cacheTimestamps = {};
final Duration _defaultCacheDuration = const Duration(minutes: 30);
/// 设置缓存
void set(String key, dynamic value, {Duration? duration}) {
_cache[key] = value;
_cacheTimestamps[key] = DateTime.now();
}
/// 获取缓存
T? get<T>(String key, {Duration? duration}) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) return null;
final cacheDuration = duration ?? _defaultCacheDuration;
if (DateTime.now().difference(timestamp) > cacheDuration) {
remove(key);
return null;
}
return _cache[key] as T?;
}
/// 移除缓存
void remove(String key) {
_cache.remove(key);
_cacheTimestamps.remove(key);
}
/// 清空缓存
void clear() {
_cache.clear();
_cacheTimestamps.clear();
}
/// 检查缓存是否存在且有效
bool isValid(String key, {Duration? duration}) {
final timestamp = _cacheTimestamps[key];
if (timestamp == null) return false;
final cacheDuration = duration ?? _defaultCacheDuration;
return DateTime.now().difference(timestamp) <= cacheDuration;
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app_state_provider.dart';
import '../../shared/providers/network_provider.dart';
import '../../shared/providers/error_provider.dart';
import '../../features/auth/providers/auth_provider.dart' as auth;
import '../../features/vocabulary/providers/vocabulary_provider.dart' as vocab;
import '../../features/comprehensive_test/providers/test_riverpod_provider.dart' as test;
/// 全局Provider配置
class GlobalProviders {
static final List<Override> overrides = [
// 这里可以添加测试时的Provider覆盖
];
static final List<ProviderObserver> observers = [
ProviderLogger(),
];
/// 获取所有核心Provider
static List<ProviderBase> get coreProviders => [
// 应用状态
appStateProvider,
networkProvider,
errorProvider,
// 认证相关
auth.authProvider,
// 词汇相关
vocab.vocabularyProvider,
// 综合测试相关
test.testProvider,
];
/// 预加载Provider
static Future<void> preloadProviders(ProviderContainer container) async {
// 预加载网络状态
container.read(networkProvider.notifier).refreshNetworkStatus();
// 认证状态会在AuthNotifier构造时自动检查
// 这里只需要读取provider来触发初始化
container.read(auth.authProvider);
}
}
/// Provider状态监听器
class ProviderLogger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} updated: $newValue');
}
@override
void didAddProvider(
ProviderBase provider,
Object? value,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} added: $value');
}
@override
void didDisposeProvider(
ProviderBase provider,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} disposed');
}
@override
void providerDidFail(
ProviderBase provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} failed: $error');
}
}

View File

@@ -0,0 +1,496 @@
import 'package:flutter/material.dart';
import '../../features/auth/screens/splash_screen.dart';
import '../../features/auth/screens/login_screen.dart';
import '../../features/auth/screens/register_screen.dart';
import '../../features/auth/screens/forgot_password_screen.dart';
import '../../features/main/screens/main_app_screen.dart';
import '../../features/learning/screens/learning_home_screen.dart';
import '../../features/vocabulary/screens/vocabulary_home_screen.dart';
import '../../features/vocabulary/screens/vocabulary_category_screen.dart';
import '../../features/vocabulary/screens/vocabulary_book_screen.dart';
import '../../features/vocabulary/screens/word_learning_screen.dart';
import '../../features/vocabulary/screens/smart_review_screen.dart';
import '../../features/vocabulary/screens/vocabulary_test_screen.dart';
import '../../features/vocabulary/screens/daily_words_screen.dart';
import '../../features/vocabulary/screens/ai_recommendation_screen.dart';
import '../../features/vocabulary/screens/word_book_screen.dart';
import '../../features/vocabulary/screens/study_plan_screen.dart';
import '../../features/vocabulary/models/word_model.dart';
import '../../features/vocabulary/models/vocabulary_book_model.dart';
import '../../features/vocabulary/models/review_models.dart';
import '../../features/listening/screens/listening_home_screen.dart';
import '../../features/listening/screens/listening_category_screen.dart';
import '../../features/listening/screens/listening_exercise_detail_screen.dart';
import '../../features/listening/screens/listening_difficulty_screen.dart';
import '../../features/listening/screens/listening_stats_screen.dart';
import '../../features/listening/models/listening_exercise_model.dart';
// 移除静态数据依赖
import '../../features/reading/screens/reading_home_screen.dart';
import '../../features/writing/screens/writing_home_screen.dart';
import '../../features/speaking/screens/speaking_home_screen.dart';
import '../../features/comprehensive_test/screens/comprehensive_test_screen.dart';
import '../../features/profile/screens/profile_home_screen.dart';
import '../../features/profile/screens/profile_edit_screen.dart';
import '../../features/profile/screens/settings_screen.dart';
import '../../features/profile/screens/help_feedback_screen.dart';
import '../../features/ai/pages/ai_main_page.dart';
import '../../features/ai/pages/ai_writing_page.dart';
import '../../features/ai/pages/ai_speaking_page.dart';
import '../../features/home/screens/learning_stats_detail_screen.dart';
import '../../features/notification/screens/notification_list_screen.dart';
import '../widgets/not_found_screen.dart';
// 学习模式枚举
enum LearningMode {
normal,
review,
test
}
/// 路由名称常量
class Routes {
static const String splash = '/splash';
static const String login = '/login';
static const String register = '/register';
static const String forgotPassword = '/forgot-password';
static const String home = '/home';
static const String learning = '/learning';
static const String profile = '/profile';
static const String editProfile = '/edit-profile';
static const String settings = '/settings';
static const String helpFeedback = '/help-feedback';
static const String vocabularyHome = '/vocabulary';
static const String vocabularyCategory = '/vocabulary/category';
static const String vocabularyList = '/vocabulary/list';
static const String vocabularyBook = '/vocabulary/book';
static const String wordDetail = '/vocabulary/word';
static const String vocabularyTest = '/vocabulary/test';
static const String wordLearning = '/vocabulary/learning';
static const String smartReview = '/vocabulary/smart-review';
static const String dailyWords = '/vocabulary/daily-words';
static const String aiRecommendation = '/vocabulary/ai-recommendation';
static const String wordBook = '/vocabulary/word-book';
static const String studyPlan = '/vocabulary/study-plan';
static const String listeningHome = '/listening';
static const String listeningExercise = '/listening/exercise';
static const String listeningCategory = '/listening/category';
static const String listeningExerciseDetail = '/listening/exercise-detail';
static const String listeningDifficulty = '/listening/difficulty';
static const String listeningStats = '/listening/stats';
static const String readingHome = '/reading';
static const String readingExercise = '/reading/exercise';
static const String writingHome = '/writing';
static const String writingExercise = '/writing/exercise';
static const String speakingHome = '/speaking';
static const String speakingExercise = '/speaking/exercise';
static const String comprehensiveTest = '/comprehensive-test';
static const String ai = '/ai';
static const String aiWriting = '/ai/writing';
static const String aiSpeaking = '/ai/speaking';
static const String learningStatsDetail = '/learning-stats-detail';
static const String notifications = '/notifications';
}
/// 应用路由配置
class AppRoutes {
/// 路由映射表
static final Map<String, WidgetBuilder> _routes = {
Routes.splash: (context) => const SplashScreen(),
Routes.login: (context) => const LoginScreen(),
Routes.register: (context) => const RegisterScreen(),
Routes.forgotPassword: (context) => const ForgotPasswordScreen(),
Routes.home: (context) => const MainAppScreen(),
Routes.learning: (context) => const LearningHomeScreen(),
Routes.vocabularyHome: (context) => const VocabularyHomeScreen(),
// TODO: 这些路由需要参数暂时注释掉后续通过onGenerateRoute处理
// Routes.vocabularyList: (context) => const VocabularyBookScreen(),
// Routes.wordDetail: (context) => const WordLearningScreen(),
// Routes.vocabularyTest: (context) => const VocabularyTestScreen(),
// Routes.wordLearning: (context) => const SmartReviewScreen(),
Routes.dailyWords: (context) => const DailyWordsScreen(),
Routes.aiRecommendation: (context) => const AIRecommendationScreen(),
Routes.wordBook: (context) => const WordBookScreen(),
Routes.studyPlan: (context) => const StudyPlanScreen(),
Routes.listeningHome: (context) => const ListeningHomeScreen(),
Routes.listeningDifficulty: (context) => const ListeningDifficultyScreen(),
Routes.listeningStats: (context) => const ListeningStatsScreen(),
Routes.readingHome: (context) => const ReadingHomeScreen(),
Routes.writingHome: (context) => const WritingHomeScreen(),
Routes.speakingHome: (context) => const SpeakingHomeScreen(),
Routes.comprehensiveTest: (context) => const ComprehensiveTestScreen(),
Routes.profile: (context) => const ProfileHomeScreen(),
Routes.editProfile: (context) => const ProfileEditScreen(),
Routes.settings: (context) => const SettingsScreen(),
Routes.helpFeedback: (context) => const HelpFeedbackScreen(),
Routes.ai: (context) => const AIMainPage(),
Routes.aiWriting: (context) => const AIWritingPage(),
Routes.aiSpeaking: (context) => const AISpeakingPage(),
Routes.learningStatsDetail: (context) => const LearningStatsDetailScreen(),
Routes.notifications: (context) => const NotificationListScreen(),
// TODO: 添加其他页面路由
};
/// 获取路由映射表
static Map<String, WidgetBuilder> get routes => _routes;
/// 路由生成器
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
final String routeName = settings.name ?? '';
final arguments = settings.arguments;
// 处理带参数的词汇学习路由
switch (routeName) {
case Routes.vocabularyCategory:
if (arguments is Map<String, dynamic>) {
final category = arguments['category'];
if (category != null) {
return MaterialPageRoute(
builder: (context) => VocabularyCategoryScreen(category: category),
settings: settings,
);
}
}
break;
case Routes.vocabularyList:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
if (vocabularyBook != null) {
return MaterialPageRoute(
builder: (context) => VocabularyBookScreen(vocabularyBook: vocabularyBook),
settings: settings,
);
}
}
break;
case Routes.wordLearning:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final specificWords = arguments['specificWords'];
final mode = arguments['mode'];
if (vocabularyBook != null) {
return MaterialPageRoute(
builder: (context) => WordLearningScreen(
vocabularyBook: vocabularyBook,
specificWords: specificWords,
mode: mode ?? LearningMode.normal,
),
settings: settings,
);
}
}
break;
case Routes.vocabularyTest:
// 词汇测试路由,支持带参数和不带参数的情况
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final testType = arguments['testType'];
final questionCount = arguments['questionCount'];
return MaterialPageRoute(
builder: (context) => VocabularyTestScreen(
vocabularyBook: vocabularyBook,
testType: testType ?? TestType.vocabularyLevel,
questionCount: questionCount ?? 20,
),
settings: settings,
);
} else {
// 没有参数时,使用默认设置
return MaterialPageRoute(
builder: (context) => const VocabularyTestScreen(
testType: TestType.vocabularyLevel,
questionCount: 20,
),
settings: settings,
);
}
break;
case Routes.wordDetail:
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final reviewMode = arguments['reviewMode'];
final dailyTarget = arguments['dailyTarget'];
return MaterialPageRoute(
builder: (context) => SmartReviewScreen(
vocabularyBook: vocabularyBook,
reviewMode: reviewMode ?? ReviewMode.adaptive,
dailyTarget: dailyTarget ?? 20,
),
settings: settings,
);
}
break;
case Routes.smartReview:
// 智能复习路由,支持带参数和不带参数的情况
if (arguments is Map<String, dynamic>) {
final vocabularyBook = arguments['vocabularyBook'];
final reviewMode = arguments['reviewMode'];
final dailyTarget = arguments['dailyTarget'];
return MaterialPageRoute(
builder: (context) => SmartReviewScreen(
vocabularyBook: vocabularyBook,
reviewMode: reviewMode ?? ReviewMode.adaptive,
dailyTarget: dailyTarget ?? 20,
),
settings: settings,
);
} else {
// 没有参数时,使用默认设置
return MaterialPageRoute(
builder: (context) => const SmartReviewScreen(
reviewMode: ReviewMode.adaptive,
dailyTarget: 20,
),
settings: settings,
);
}
break;
// 听力相关路由
case Routes.listeningCategory:
if (arguments is Map<String, dynamic>) {
final type = arguments['type'] as ListeningExerciseType;
final title = arguments['title'] as String;
final category = ListeningCategory(
id: type.toString(),
name: title,
description: '${title}练习材料',
icon: Icons.headphones,
exerciseCount: 0,
type: type,
);
return MaterialPageRoute(
builder: (context) => ListeningCategoryScreen(
category: category,
),
settings: settings,
);
}
break;
case Routes.listeningExerciseDetail:
if (arguments is Map<String, dynamic>) {
final exerciseId = arguments['exerciseId'];
if (exerciseId != null) {
return MaterialPageRoute(
builder: (context) => ListeningExerciseDetailScreen(
exerciseId: exerciseId,
),
settings: settings,
);
}
}
break;
}
// 默认路由处理
final WidgetBuilder? builder = _routes[routeName];
if (builder != null) {
return MaterialPageRoute(
builder: builder,
settings: settings,
);
}
// 未找到路由时的处理
return MaterialPageRoute(
builder: (context) => const NotFoundScreen(),
settings: settings,
);
}
/// 路由守卫 - 检查是否需要认证
static bool requiresAuth(String routeName) {
const publicRoutes = [
Routes.splash,
Routes.login,
Routes.register,
Routes.forgotPassword,
];
return !publicRoutes.contains(routeName);
}
/// 获取初始路由
static String getInitialRoute(bool isLoggedIn) {
return isLoggedIn ? Routes.home : Routes.splash;
}
}
/// 启动页面
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
// 初始化应用配置
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
// 导航到登录页面,让用户进行认证
Navigator.of(context).pushReplacementNamed(Routes.login);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// TODO: 添加应用Logo
Icon(
Icons.school,
size: 100,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 24),
Text(
'AI英语学习',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(height: 16),
Text(
'智能化英语学习平台',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const SizedBox(height: 48),
const CircularProgressIndicator(),
],
),
),
);
}
}
/// 404页面
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('页面未找到'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 100,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 24),
Text(
'404',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 16),
Text(
'抱歉,您访问的页面不存在',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
Routes.home,
(route) => false,
);
},
child: const Text('返回首页'),
),
],
),
),
);
}
}
/// 路由导航辅助类
class AppNavigator {
/// 导航到指定页面
static Future<T?> push<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
}) {
return Navigator.of(context).pushNamed<T>(
routeName,
arguments: arguments,
);
}
/// 替换当前页面
static Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
TO? result,
}) {
return Navigator.of(context).pushReplacementNamed<T, TO>(
routeName,
arguments: arguments,
result: result,
);
}
/// 清空栈并导航到指定页面
static Future<T?> pushAndRemoveUntil<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
bool Function(Route<dynamic>)? predicate,
}) {
return Navigator.of(context).pushNamedAndRemoveUntil<T>(
routeName,
predicate ?? (route) => false,
arguments: arguments,
);
}
/// 返回上一页
static void pop<T extends Object?>(BuildContext context, [T? result]) {
Navigator.of(context).pop<T>(result);
}
/// 返回到指定页面
static void popUntil(BuildContext context, String routeName) {
Navigator.of(context).popUntil(ModalRoute.withName(routeName));
}
/// 检查是否可以返回
static bool canPop(BuildContext context) {
return Navigator.of(context).canPop();
}
}

View 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();
}
}

View 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;
}
}

View 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}');
}
}
}

View 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');
}
}
}
}

View 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();
}
}

View 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,
);
}
}

View 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';
}

View 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');
}
}
}

View File

@@ -0,0 +1,150 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
/// 本地存储服务
class StorageService {
static SharedPreferences? _prefs;
/// 初始化存储服务
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
/// 获取SharedPreferences实例
static SharedPreferences get _instance {
if (_prefs == null) {
throw Exception('StorageService not initialized. Call StorageService.init() first.');
}
return _prefs!;
}
/// 存储字符串
static Future<bool> setString(String key, String value) async {
return await _instance.setString(key, value);
}
/// 获取字符串
static String? getString(String key) {
return _instance.getString(key);
}
/// 存储整数
static Future<bool> setInt(String key, int value) async {
return await _instance.setInt(key, value);
}
/// 获取整数
static int? getInt(String key) {
return _instance.getInt(key);
}
/// 存储双精度浮点数
static Future<bool> setDouble(String key, double value) async {
return await _instance.setDouble(key, value);
}
/// 获取双精度浮点数
static double? getDouble(String key) {
return _instance.getDouble(key);
}
/// 存储布尔值
static Future<bool> setBool(String key, bool value) async {
return await _instance.setBool(key, value);
}
/// 获取布尔值
static bool? getBool(String key) {
return _instance.getBool(key);
}
/// 存储字符串列表
static Future<bool> setStringList(String key, List<String> value) async {
return await _instance.setStringList(key, value);
}
/// 获取字符串列表
static List<String>? getStringList(String key) {
return _instance.getStringList(key);
}
/// 存储对象JSON序列化
static Future<bool> setObject(String key, Map<String, dynamic> value) async {
final jsonString = jsonEncode(value);
return await setString(key, jsonString);
}
/// 获取对象JSON反序列化
static Map<String, dynamic>? getObject(String key) {
final jsonString = getString(key);
if (jsonString == null) return null;
try {
return jsonDecode(jsonString) as Map<String, dynamic>;
} catch (e) {
print('Error decoding JSON for key $key: $e');
return null;
}
}
/// 存储对象列表JSON序列化
static Future<bool> setObjectList(String key, List<Map<String, dynamic>> value) async {
final jsonString = jsonEncode(value);
return await setString(key, jsonString);
}
/// 获取对象列表JSON反序列化
static List<Map<String, dynamic>>? getObjectList(String key) {
final jsonString = getString(key);
if (jsonString == null) return null;
try {
final decoded = jsonDecode(jsonString) as List;
return decoded.cast<Map<String, dynamic>>();
} catch (e) {
print('Error decoding JSON list for key $key: $e');
return null;
}
}
/// 检查键是否存在
static bool containsKey(String key) {
return _instance.containsKey(key);
}
/// 删除指定键
static Future<bool> remove(String key) async {
return await _instance.remove(key);
}
/// 清空所有数据
static Future<bool> clear() async {
return await _instance.clear();
}
/// 获取所有键
static Set<String> getKeys() {
return _instance.getKeys();
}
/// 重新加载数据
static Future<void> reload() async {
await _instance.reload();
}
}
/// 存储键名常量
class StorageKeys {
static const String accessToken = 'access_token';
static const String refreshToken = 'refresh_token';
static const String userInfo = 'user_info';
static const String appSettings = 'app_settings';
static const String learningProgress = 'learning_progress';
static const String vocabularyCache = 'vocabulary_cache';
static const String studyHistory = 'study_history';
static const String offlineData = 'offline_data';
static const String themeMode = 'theme_mode';
static const String language = 'language';
static const String firstLaunch = 'first_launch';
static const String onboardingCompleted = 'onboarding_completed';
}

View File

@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
/// 应用颜色常量
class AppColors {
// 私有构造函数,防止实例化
AppColors._();
// ============ 浅色主题颜色 ============
/// 主色调
static const Color primary = Color(0xFF1976D2);
static const Color onPrimary = Color(0xFFFFFFFF);
static const Color primaryContainer = Color(0xFFBBDEFB);
static const Color onPrimaryContainer = Color(0xFF0D47A1);
/// 次要色调
static const Color secondary = Color(0xFF03DAC6);
static const Color onSecondary = Color(0xFF000000);
static const Color secondaryContainer = Color(0xFFB2DFDB);
static const Color onSecondaryContainer = Color(0xFF004D40);
/// 第三色调
static const Color tertiary = Color(0xFF9C27B0);
static const Color onTertiary = Color(0xFFFFFFFF);
static const Color tertiaryContainer = Color(0xFFE1BEE7);
static const Color onTertiaryContainer = Color(0xFF4A148C);
/// 错误色
static const Color error = Color(0xFFD32F2F);
static const Color onError = Color(0xFFFFFFFF);
static const Color errorContainer = Color(0xFFFFCDD2);
static const Color onErrorContainer = Color(0xFFB71C1C);
/// 背景颜色
static const Color background = Color(0xFFFFFBFE);
static const Color onBackground = Color(0xFF1C1B1F);
/// 表面色
static const Color surface = Color(0xFFFFFFFF);
static const Color onSurface = Color(0xFF1C1B1F);
static const Color surfaceTint = Color(0xFF1976D2);
/// 表面变体色
static const Color surfaceVariant = Color(0xFFF3F3F3);
static const Color onSurfaceVariant = Color(0xFF49454F);
/// 轮廓色
static const Color outline = Color(0xFF79747E);
static const Color outlineVariant = Color(0xFFCAC4D0);
/// 阴影颜色
static const Color shadow = Color(0xFF000000);
static const Color scrim = Color(0xFF000000);
/// 反转颜色
static const Color inverseSurface = Color(0xFF313033);
static const Color onInverseSurface = Color(0xFFF4EFF4);
static const Color inversePrimary = Color(0xFF90CAF9);
// ============ 深色主题颜色 ============
/// 主色调 - 深色
static const Color primaryDark = Color(0xFF90CAF9);
static const Color onPrimaryDark = Color(0xFF003C8F);
static const Color primaryContainerDark = Color(0xFF1565C0);
static const Color onPrimaryContainerDark = Color(0xFFE3F2FD);
/// 次要色调 - 深色
static const Color secondaryDark = Color(0xFF80CBC4);
static const Color onSecondaryDark = Color(0xFF00251A);
static const Color secondaryContainerDark = Color(0xFF00695C);
static const Color onSecondaryContainerDark = Color(0xFFE0F2F1);
/// 第三色调 - 深色
static const Color tertiaryDark = Color(0xFFCE93D8);
static const Color onTertiaryDark = Color(0xFF4A148C);
static const Color tertiaryContainerDark = Color(0xFF7B1FA2);
static const Color onTertiaryContainerDark = Color(0xFFF3E5F5);
/// 错误色 - 深色
static const Color errorDark = Color(0xFFEF5350);
static const Color onErrorDark = Color(0xFF690005);
static const Color errorContainerDark = Color(0xFFD32F2F);
static const Color onErrorContainerDark = Color(0xFFFFEBEE);
/// 背景颜色 - 深色
static const Color backgroundDark = Color(0xFF1C1B1F);
static const Color onBackgroundDark = Color(0xFFE6E1E5);
/// 表面色 - 深色
static const Color surfaceDark = Color(0xFF121212);
static const Color onSurfaceDark = Color(0xFFE6E1E5);
static const Color surfaceTintDark = Color(0xFF90CAF9);
/// 表面变体色 - 深色
static const Color surfaceVariantDark = Color(0xFF49454F);
static const Color onSurfaceVariantDark = Color(0xFFCAC4D0);
/// 轮廓色 - 深色
static const Color outlineDark = Color(0xFF938F99);
static const Color outlineVariantDark = Color(0xFF49454F);
/// 阴影颜色 - 深色
static const Color shadowDark = Color(0xFF000000);
static const Color scrimDark = Color(0xFF000000);
/// 反转色 - 深色
static const Color inverseSurfaceDark = Color(0xFFE6E1E5);
static const Color onInverseSurfaceDark = Color(0xFF313033);
static const Color inversePrimaryDark = Color(0xFF1976D2);
// ============ 功能性颜色 ============
/// 成功色
static const Color success = Color(0xFF4CAF50);
static const Color onSuccess = Color(0xFFFFFFFF);
static const Color successContainer = Color(0xFFE8F5E8);
static const Color onSuccessContainer = Color(0xFF1B5E20);
/// 警告色
static const Color warning = Color(0xFFFF9800);
static const Color onWarning = Color(0xFFFFFFFF);
static const Color warningContainer = Color(0xFFFFF3E0);
static const Color onWarningContainer = Color(0xFFE65100);
/// 信息色
static const Color info = Color(0xFF2196F3);
static const Color onInfo = Color(0xFFFFFFFF);
static const Color infoContainer = Color(0xFFE3F2FD);
static const Color onInfoContainer = Color(0xFF0D47A1);
// ============ 学习模块颜色 ============
/// 词汇学习
static const Color vocabulary = Color(0xFF9C27B0);
static const Color onVocabulary = Color(0xFFFFFFFF);
static const Color vocabularyContainer = Color(0xFFF3E5F5);
static const Color onVocabularyContainer = Color(0xFF4A148C);
static const Color vocabularyDark = Color(0xFFBA68C8);
/// 听力训练
static const Color listening = Color(0xFF00BCD4);
static const Color onListening = Color(0xFFFFFFFF);
static const Color listeningContainer = Color(0xFFE0F7FA);
static const Color onListeningContainer = Color(0xFF006064);
static const Color listeningDark = Color(0xFF4DD0E1);
/// 阅读理解
static const Color reading = Color(0xFF4CAF50);
static const Color onReading = Color(0xFFFFFFFF);
static const Color readingContainer = Color(0xFFE8F5E8);
static const Color onReadingContainer = Color(0xFF1B5E20);
static const Color readingDark = Color(0xFF81C784);
/// 写作练习
static const Color writing = Color(0xFFFF5722);
static const Color onWriting = Color(0xFFFFFFFF);
static const Color writingContainer = Color(0xFFFBE9E7);
static const Color onWritingContainer = Color(0xFFBF360C);
static const Color writingDark = Color(0xFFFF8A65);
/// 口语练习
static const Color speaking = Color(0xFFE91E63);
static const Color onSpeaking = Color(0xFFFFFFFF);
static const Color speakingContainer = Color(0xFFFCE4EC);
static const Color onSpeakingContainer = Color(0xFF880E4F);
static const Color speakingDark = Color(0xFFF06292);
// ============ 等级颜色 ============
/// 初级
static const Color beginner = Color(0xFF4CAF50);
static const Color onBeginner = Color(0xFFFFFFFF);
static const Color beginnerDark = Color(0xFF81C784);
/// 中级
static const Color intermediate = Color(0xFFFF9800);
static const Color onIntermediate = Color(0xFFFFFFFF);
static const Color intermediateDark = Color(0xFFFFB74D);
/// 高级
static const Color advanced = Color(0xFFF44336);
static const Color onAdvanced = Color(0xFFFFFFFF);
static const Color advancedDark = Color(0xFFEF5350);
// ============ 进度颜色 ============
/// 进度条背景
static const Color progressBackground = Color(0xFFE0E0E0);
/// 进度条前景
static const Color progressForeground = Color(0xFF2196F3);
/// 完成状态
static const Color completed = Color(0xFF4CAF50);
/// 进行中状态
static const Color inProgress = Color(0xFFFF9800);
/// 未开始状态
static const Color notStarted = Color(0xFFBDBDBD);
/// 进度等级颜色
static const Color progressLow = Color(0xFFF44336);
static const Color progressLowDark = Color(0xFFEF5350);
static const Color progressMedium = Color(0xFFFF9800);
static const Color progressMediumDark = Color(0xFFFFB74D);
static const Color progressHigh = Color(0xFF4CAF50);
static const Color progressHighDark = Color(0xFF81C784);
// ============ 特殊颜色 ============
/// 分割线
static const Color divider = Color(0xFFE0E0E0);
/// 禁用状态
static const Color disabled = Color(0xFFBDBDBD);
static const Color onDisabled = Color(0xFF757575);
/// 透明度变体
static Color get primaryWithOpacity => primary.withValues(alpha: 0.12);
static Color get secondaryWithOpacity => secondary.withValues(alpha: 0.12);
static Color get errorWithOpacity => error.withValues(alpha: 0.12);
static Color get successWithOpacity => success.withValues(alpha: 0.12);
static Color get warningWithOpacity => warning.withValues(alpha: 0.12);
static Color get infoWithOpacity => info.withValues(alpha: 0.12);
// ============ 渐变色 ============
/// 主要渐变
static const LinearGradient primaryGradient = LinearGradient(
colors: [Color(0xFF2196F3), Color(0xFF21CBF3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 次要渐变
static const LinearGradient secondaryGradient = LinearGradient(
colors: [Color(0xFF03DAC6), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 词汇渐变
static const LinearGradient vocabularyGradient = LinearGradient(
colors: [Color(0xFF9C27B0), Color(0xFFE91E63)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 听力渐变
static const LinearGradient listeningGradient = LinearGradient(
colors: [Color(0xFF00BCD4), Color(0xFF2196F3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 阅读渐变
static const LinearGradient readingGradient = LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF8BC34A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 写作渐变
static const LinearGradient writingGradient = LinearGradient(
colors: [Color(0xFFFF5722), Color(0xFFFF9800)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 口语渐变
static const LinearGradient speakingGradient = LinearGradient(
colors: [Color(0xFFE91E63), Color(0xFF9C27B0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}

View File

@@ -0,0 +1,365 @@
/// 应用尺寸配置
class AppDimensions {
// 私有构造函数,防止实例化
AppDimensions._();
// ============ 间距 ============
/// 极小间距
static const double spacingXs = 4.0;
/// 小间距
static const double spacingSm = 8.0;
/// 中等间距
static const double spacingMd = 16.0;
/// 大间距
static const double spacingLg = 24.0;
/// 超大间距
static const double spacingXl = 32.0;
/// 超超大间距
static const double spacingXxl = 48.0;
// ============ 内边距 ============
/// 页面内边距
static const double pagePadding = spacingMd;
/// 卡片内边距
static const double cardPadding = spacingMd;
/// 按钮内边距
static const double buttonPadding = spacingMd;
/// 输入框内边距
static const double inputPadding = spacingMd;
/// 列表项内边距
static const double listItemPadding = spacingMd;
/// 对话框内边距
static const double dialogPadding = spacingLg;
/// 底部导航栏内边距
static const double bottomNavPadding = spacingSm;
/// AppBar内边距
static const double appBarPadding = spacingMd;
// ============ 外边距 ============
/// 页面外边距
static const double pageMargin = spacingMd;
/// 卡片外边距
static const double cardMargin = spacingSm;
/// 按钮外边距
static const double buttonMargin = spacingSm;
/// 输入框外边距
static const double inputMargin = spacingSm;
/// 列表项外边距
static const double listItemMargin = spacingXs;
/// 对话框外边距
static const double dialogMargin = spacingLg;
// ============ 圆角半径 ============
/// 极小圆角
static const double radiusXs = 4.0;
/// 小圆角
static const double radiusSm = 8.0;
/// 中等圆角
static const double radiusMd = 12.0;
/// 大圆角
static const double radiusLg = 16.0;
/// 超大圆角
static const double radiusXl = 20.0;
/// 圆形
static const double radiusCircle = 999.0;
// ============ 组件圆角 ============
/// 按钮圆角
static const double buttonRadius = radiusSm;
/// 卡片圆角
static const double cardRadius = radiusMd;
/// 输入框圆角
static const double inputRadius = radiusSm;
/// 对话框圆角
static const double dialogRadius = radiusLg;
/// 底部弹窗圆角
static const double bottomSheetRadius = radiusLg;
/// 芯片圆角
static const double chipRadius = radiusLg;
/// 头像圆角
static const double avatarRadius = radiusCircle;
/// 图片圆角
static const double imageRadius = radiusSm;
// ============ 高度 ============
/// AppBar高度
static const double appBarHeight = 56.0;
/// 底部导航栏高度
static const double bottomNavHeight = 80.0;
/// 标签栏高度
static const double tabBarHeight = 48.0;
/// 按钮高度
static const double buttonHeight = 48.0;
/// 小按钮高度
static const double buttonHeightSm = 36.0;
/// 大按钮高度
static const double buttonHeightLg = 56.0;
/// 输入框高度
static const double inputHeight = 56.0;
/// 列表项高度
static const double listItemHeight = 56.0;
/// 小列表项高度
static const double listItemHeightSm = 48.0;
/// 大列表项高度
static const double listItemHeightLg = 72.0;
/// 工具栏高度
static const double toolbarHeight = 56.0;
/// 搜索栏高度
static const double searchBarHeight = 48.0;
/// 进度条高度
static const double progressBarHeight = 4.0;
/// 分割线高度
static const double dividerHeight = 1.0;
// ============ 宽度 ============
/// 最小按钮宽度
static const double buttonMinWidth = 64.0;
/// 侧边栏宽度
static const double drawerWidth = 304.0;
/// 分割线宽度
static const double dividerWidth = 1.0;
/// 边框宽度
static const double borderWidth = 1.0;
/// 粗边框宽度
static const double borderWidthThick = 2.0;
// ============ 图标尺寸 ============
/// 极小图标
static const double iconXs = 16.0;
/// 小图标
static const double iconSm = 20.0;
/// 中等图标
static const double iconMd = 24.0;
/// 大图标
static const double iconLg = 32.0;
/// 超大图标
static const double iconXl = 48.0;
/// 超超大图标
static const double iconXxl = 64.0;
// ============ 头像尺寸 ============
/// 小头像
static const double avatarSm = 32.0;
/// 中等头像
static const double avatarMd = 48.0;
/// 大头像
static const double avatarLg = 64.0;
/// 超大头像
static const double avatarXl = 96.0;
// ============ 阴影 ============
/// 阴影偏移
static const double shadowOffset = 2.0;
/// 阴影模糊半径
static const double shadowBlurRadius = 8.0;
/// 阴影扩散半径
static const double shadowSpreadRadius = 0.0;
// ============ 动画持续时间 ============
/// 快速动画
static const Duration animationFast = Duration(milliseconds: 150);
/// 中等动画
static const Duration animationMedium = Duration(milliseconds: 300);
/// 慢速动画
static const Duration animationSlow = Duration(milliseconds: 500);
// ============ 学习相关尺寸 ============
/// 单词卡片高度
static const double wordCardHeight = 200.0;
/// 单词卡片宽度
static const double wordCardWidth = 300.0;
/// 进度圆环大小
static const double progressCircleSize = 120.0;
/// 等级徽章大小
static const double levelBadgeSize = 40.0;
/// 成就徽章大小
static const double achievementBadgeSize = 60.0;
/// 音频播放器高度
static const double audioPlayerHeight = 80.0;
/// 练习题选项高度
static const double exerciseOptionHeight = 48.0;
/// 学习统计卡片高度
static const double statsCardHeight = 120.0;
// ============ 响应式断点 ============
/// 手机断点
static const double mobileBreakpoint = 600.0;
/// 平板断点
static const double tabletBreakpoint = 900.0;
/// 桌面断点
static const double desktopBreakpoint = 1200.0;
// ============ 最大宽度 ============
/// 内容最大宽度
static const double maxContentWidth = 1200.0;
/// 对话框最大宽度
static const double maxDialogWidth = 560.0;
/// 卡片最大宽度
static const double maxCardWidth = 400.0;
// ============ 最小尺寸 ============
/// 最小触摸目标尺寸
static const double minTouchTarget = 48.0;
/// 最小按钮尺寸
static const double minButtonSize = 36.0;
// ============ 网格布局 ============
/// 网格间距
static const double gridSpacing = spacingSm;
/// 网格交叉轴间距
static const double gridCrossAxisSpacing = spacingSm;
/// 网格主轴间距
static const double gridMainAxisSpacing = spacingSm;
/// 网格子项宽高比
static const double gridChildAspectRatio = 1.0;
// ============ 列表布局 ============
/// 列表分割线缩进
static const double listDividerIndent = spacingMd;
/// 列表分割线结束缩进
static const double listDividerEndIndent = spacingMd;
// ============ 浮动操作按钮 ============
/// 浮动操作按钮大小
static const double fabSize = 56.0;
/// 小浮动操作按钮大小
static const double fabSizeSmall = 40.0;
/// 大浮动操作按钮大小
static const double fabSizeLarge = 96.0;
/// 浮动操作按钮边距
static const double fabMargin = spacingMd;
// ============ 辅助方法 ============
/// 根据屏幕宽度获取响应式间距
static double getResponsiveSpacing(double screenWidth) {
if (screenWidth < mobileBreakpoint) {
return spacingSm;
} else if (screenWidth < tabletBreakpoint) {
return spacingMd;
} else {
return spacingLg;
}
}
/// 根据屏幕宽度获取响应式内边距
static double getResponsivePadding(double screenWidth) {
if (screenWidth < mobileBreakpoint) {
return spacingMd;
} else if (screenWidth < tabletBreakpoint) {
return spacingLg;
} else {
return spacingXl;
}
}
/// 根据屏幕宽度判断是否为移动设备
static bool isMobile(double screenWidth) {
return screenWidth < mobileBreakpoint;
}
/// 根据屏幕宽度判断是否为平板设备
static bool isTablet(double screenWidth) {
return screenWidth >= mobileBreakpoint && screenWidth < desktopBreakpoint;
}
/// 根据屏幕宽度判断是否为桌面设备
static bool isDesktop(double screenWidth) {
return screenWidth >= desktopBreakpoint;
}
}

View File

@@ -0,0 +1,437 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
/// 应用文本样式配置
class AppTextStyles {
// 私有构造函数,防止实例化
AppTextStyles._();
// ============ 字体家族 ============
/// 默认字体 - 使用系统默认,不指定字体名称
static const String? defaultFontFamily = null;
/// 中文字体 - 使用系统默认,不指定字体名称
static const String? chineseFontFamily = null;
/// 等宽字体 - 使用系统默认,不指定字体名称
static const String? monospaceFontFamily = null;
// ============ 字体权重 ============
static const FontWeight light = FontWeight.w300;
static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.w700;
static const FontWeight extraBold = FontWeight.w800;
// ============ 基础文本样式 ============
/// 标题样式
static const TextStyle displayLarge = TextStyle(
fontSize: 57,
fontWeight: bold,
letterSpacing: -0.25,
height: 1.12,
color: AppColors.onSurface,
);
static const TextStyle displayMedium = TextStyle(
fontSize: 45,
fontWeight: bold,
letterSpacing: 0,
height: 1.16,
color: AppColors.onSurface,
);
static const TextStyle displaySmall = TextStyle(
fontSize: 36,
fontWeight: bold,
letterSpacing: 0,
height: 1.22,
color: AppColors.onSurface,
);
/// 标题样式
static const TextStyle headlineLarge = TextStyle(
fontSize: 32,
fontWeight: bold,
letterSpacing: 0,
height: 1.25,
color: AppColors.onSurface,
);
static const TextStyle headlineMedium = TextStyle(
fontSize: 28,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.29,
color: AppColors.onSurface,
);
static const TextStyle headlineSmall = TextStyle(
fontSize: 24,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.33,
color: AppColors.onSurface,
);
/// 标题样式
static const TextStyle titleLarge = TextStyle(
fontSize: 22,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.27,
color: AppColors.onSurface,
);
static const TextStyle titleMedium = TextStyle(
fontSize: 16,
fontWeight: medium,
letterSpacing: 0.15,
height: 1.50,
color: AppColors.onSurface,
);
static const TextStyle titleSmall = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.onSurface,
);
/// 标签样式
static const TextStyle labelLarge = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.onSurface,
);
static const TextStyle labelMedium = TextStyle(
fontSize: 12,
fontWeight: medium,
letterSpacing: 0.5,
height: 1.33,
color: AppColors.onSurface,
);
static const TextStyle labelSmall = TextStyle(
fontSize: 11,
fontWeight: medium,
letterSpacing: 0.5,
height: 1.45,
color: AppColors.onSurface,
);
/// 正文样式
static const TextStyle bodyLarge = TextStyle(
fontSize: 16,
fontWeight: regular,
letterSpacing: 0.5,
height: 1.50,
color: AppColors.onSurface,
);
static const TextStyle bodyMedium = TextStyle(
fontSize: 14,
fontWeight: regular,
letterSpacing: 0.25,
height: 1.43,
color: AppColors.onSurface,
);
static const TextStyle bodySmall = TextStyle(
fontSize: 12,
fontWeight: regular,
letterSpacing: 0.4,
height: 1.33,
color: AppColors.onSurfaceVariant,
);
// ============ 自定义文本样式 ============
/// 按钮文本样式
static const TextStyle buttonLarge = TextStyle(
fontSize: 16,
fontWeight: semiBold,
letterSpacing: 0.1,
height: 1.25,
color: AppColors.onPrimary,
);
static const TextStyle buttonMedium = TextStyle(
fontSize: 14,
fontWeight: semiBold,
letterSpacing: 0.1,
height: 1.29,
color: AppColors.onPrimary,
);
static const TextStyle buttonSmall = TextStyle(
fontSize: 12,
fontWeight: medium,
letterSpacing: 0.5,
height: 1.33,
color: AppColors.onPrimary,
);
/// 输入框文本样式
static const TextStyle inputText = TextStyle(
fontSize: 16,
fontWeight: regular,
letterSpacing: 0.5,
height: 1.50,
color: AppColors.onSurface,
);
static const TextStyle inputLabel = TextStyle(
fontSize: 16,
fontWeight: medium,
letterSpacing: 0.15,
height: 1.50,
color: AppColors.onSurfaceVariant,
);
static const TextStyle inputHint = TextStyle(
fontSize: 16,
fontWeight: regular,
letterSpacing: 0.5,
height: 1.50,
color: AppColors.onSurfaceVariant,
);
static const TextStyle inputError = TextStyle(
fontSize: 12,
fontWeight: regular,
letterSpacing: 0.4,
height: 1.33,
color: AppColors.error,
);
/// 导航文本样式
static const TextStyle navigationLabel = TextStyle(
fontSize: 12,
fontWeight: medium,
letterSpacing: 0.5,
height: 1.33,
color: AppColors.onSurface,
);
static const TextStyle tabLabel = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.onSurface,
);
/// 卡片文本样式
static const TextStyle cardTitle = TextStyle(
fontSize: 18,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.33,
color: AppColors.onSurface,
);
static const TextStyle cardSubtitle = TextStyle(
fontSize: 14,
fontWeight: regular,
letterSpacing: 0.25,
height: 1.43,
color: AppColors.onSurfaceVariant,
);
static const TextStyle cardBody = TextStyle(
fontSize: 14,
fontWeight: regular,
letterSpacing: 0.25,
height: 1.43,
color: AppColors.onSurface,
);
/// 列表文本样式
static const TextStyle listTitle = TextStyle(
fontSize: 16,
fontWeight: medium,
letterSpacing: 0.15,
height: 1.50,
color: AppColors.onSurface,
);
static const TextStyle listSubtitle = TextStyle(
fontSize: 14,
fontWeight: regular,
letterSpacing: 0.25,
height: 1.43,
color: AppColors.onSurfaceVariant,
);
/// 学习相关文本样式
static const TextStyle wordText = TextStyle(
fontSize: 24,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.33,
color: AppColors.onSurface,
);
static const TextStyle phoneticText = TextStyle(
fontSize: 16,
fontWeight: regular,
letterSpacing: 0.5,
height: 1.50,
color: AppColors.onSurfaceVariant,
fontFamily: monospaceFontFamily,
);
static const TextStyle definitionText = TextStyle(
fontSize: 16,
fontWeight: regular,
letterSpacing: 0.5,
height: 1.50,
color: AppColors.onSurface,
);
static const TextStyle exampleText = TextStyle(
fontSize: 14,
fontWeight: regular,
letterSpacing: 0.25,
height: 1.43,
color: AppColors.onSurfaceVariant,
fontStyle: FontStyle.italic,
);
/// 分数和统计文本样式
static const TextStyle scoreText = TextStyle(
fontSize: 32,
fontWeight: bold,
letterSpacing: 0,
height: 1.25,
color: AppColors.primary,
);
static const TextStyle statisticNumber = TextStyle(
fontSize: 24,
fontWeight: semiBold,
letterSpacing: 0,
height: 1.33,
color: AppColors.onSurface,
);
static const TextStyle statisticLabel = TextStyle(
fontSize: 12,
fontWeight: medium,
letterSpacing: 0.5,
height: 1.33,
color: AppColors.onSurfaceVariant,
);
/// 状态文本样式
static const TextStyle successText = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.success,
);
static const TextStyle warningText = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.warning,
);
static const TextStyle errorText = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.error,
);
static const TextStyle infoText = TextStyle(
fontSize: 14,
fontWeight: medium,
letterSpacing: 0.1,
height: 1.43,
color: AppColors.info,
);
// ============ Material 3 文本主题 ============
/// Material 3 文本主题
static const TextTheme textTheme = TextTheme(
displayLarge: displayLarge,
displayMedium: displayMedium,
displaySmall: displaySmall,
headlineLarge: headlineLarge,
headlineMedium: headlineMedium,
headlineSmall: headlineSmall,
titleLarge: titleLarge,
titleMedium: titleMedium,
titleSmall: titleSmall,
labelLarge: labelLarge,
labelMedium: labelMedium,
labelSmall: labelSmall,
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
);
// ============ 辅助方法 ============
/// 根据主题亮度调整文本颜色
static TextStyle adaptiveTextStyle(TextStyle style, Brightness brightness) {
if (brightness == Brightness.dark) {
return style.copyWith(
color: _adaptColorForDarkTheme(style.color ?? AppColors.onSurface),
);
}
return style;
}
/// 为深色主题调整颜色
static Color _adaptColorForDarkTheme(Color color) {
if (color == AppColors.onSurface) {
return AppColors.onSurfaceDark;
} else if (color == AppColors.onSurfaceVariant) {
return AppColors.onSurfaceVariantDark;
} else if (color == AppColors.primary) {
return AppColors.primaryDark;
}
return color;
}
/// 创建带有特定颜色的文本样式
static TextStyle withColor(TextStyle style, Color color) {
return style.copyWith(color: color);
}
/// 创建带有特定字体大小的文本样式
static TextStyle withFontSize(TextStyle style, double fontSize) {
return style.copyWith(fontSize: fontSize);
}
/// 创建带有特定字体权重的文本样式
static TextStyle withFontWeight(TextStyle style, FontWeight fontWeight) {
return style.copyWith(fontWeight: fontWeight);
}
/// 创建带有特定行高的文本样式
static TextStyle withHeight(TextStyle style, double height) {
return style.copyWith(height: height);
}
/// 创建带有特定字母间距的文本样式
static TextStyle withLetterSpacing(TextStyle style, double letterSpacing) {
return style.copyWith(letterSpacing: letterSpacing);
}
}

View File

@@ -0,0 +1,424 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_text_styles.dart';
import 'app_dimensions.dart';
/// 应用主题配置
class AppTheme {
// 私有构造函数,防止实例化
AppTheme._();
// ============ 亮色主题 ============
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// 禁用Google Fonts使用系统默认字体
fontFamily: null, // 不指定字体,使用系统默认
// 颜色方案
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
onPrimary: AppColors.onPrimary,
primaryContainer: AppColors.primaryContainer,
onPrimaryContainer: AppColors.onPrimaryContainer,
secondary: AppColors.secondary,
onSecondary: AppColors.onSecondary,
secondaryContainer: AppColors.secondaryContainer,
onSecondaryContainer: AppColors.onSecondaryContainer,
tertiary: AppColors.tertiary,
onTertiary: AppColors.onTertiary,
tertiaryContainer: AppColors.tertiaryContainer,
onTertiaryContainer: AppColors.onTertiaryContainer,
error: AppColors.error,
onError: AppColors.onError,
errorContainer: AppColors.errorContainer,
onErrorContainer: AppColors.onErrorContainer,
surface: AppColors.surface,
onSurface: AppColors.onSurface,
surfaceContainerHighest: AppColors.surfaceVariant,
onSurfaceVariant: AppColors.onSurfaceVariant,
outline: AppColors.outline,
outlineVariant: AppColors.outlineVariant,
shadow: AppColors.shadow,
scrim: AppColors.scrim,
inverseSurface: AppColors.inverseSurface,
onInverseSurface: AppColors.onInverseSurface,
inversePrimary: AppColors.inversePrimary,
surfaceTint: AppColors.surfaceTint,
),
// 文本主题
textTheme: AppTextStyles.textTheme,
// AppBar主题
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surface,
foregroundColor: AppColors.onSurface,
elevation: 0,
scrolledUnderElevation: 1,
centerTitle: true,
titleTextStyle: AppTextStyles.titleLarge,
toolbarHeight: AppDimensions.appBarHeight,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
textStyle: AppTextStyles.buttonMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.buttonRadius),
),
minimumSize: const Size(AppDimensions.buttonMinWidth, AppDimensions.buttonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.buttonPadding,
vertical: AppDimensions.spacingSm,
),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
textStyle: AppTextStyles.buttonMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.buttonRadius),
),
minimumSize: const Size(AppDimensions.buttonMinWidth, AppDimensions.buttonHeight),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
textStyle: AppTextStyles.buttonMedium,
side: const BorderSide(color: AppColors.outline),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.buttonRadius),
),
minimumSize: const Size(AppDimensions.buttonMinWidth, AppDimensions.buttonHeight),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
textStyle: AppTextStyles.buttonMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.buttonRadius),
),
minimumSize: const Size(AppDimensions.buttonMinWidth, AppDimensions.buttonHeight),
),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.inputRadius),
borderSide: const BorderSide(color: AppColors.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.inputRadius),
borderSide: const BorderSide(color: AppColors.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.inputRadius),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.inputRadius),
borderSide: const BorderSide(color: AppColors.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.inputRadius),
borderSide: const BorderSide(color: AppColors.error, width: 2),
),
labelStyle: AppTextStyles.inputLabel,
hintStyle: AppTextStyles.inputHint,
errorStyle: AppTextStyles.inputError,
contentPadding: const EdgeInsets.all(AppDimensions.inputPadding),
),
// 卡片主题
cardTheme: CardThemeData(
color: AppColors.surface,
elevation: 1,
shadowColor: AppColors.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.cardRadius),
),
margin: const EdgeInsets.all(AppDimensions.cardMargin),
),
// 底部导航栏主题
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: AppColors.surface,
selectedItemColor: AppColors.primary,
unselectedItemColor: AppColors.onSurfaceVariant,
selectedLabelStyle: AppTextStyles.navigationLabel,
unselectedLabelStyle: AppTextStyles.navigationLabel,
type: BottomNavigationBarType.fixed,
elevation: 8,
),
// 标签栏主题
tabBarTheme: TabBarThemeData(
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.onSurfaceVariant,
labelStyle: AppTextStyles.tabLabel,
unselectedLabelStyle: AppTextStyles.tabLabel,
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(color: AppColors.primary, width: 2),
),
),
// 芯片主题
chipTheme: ChipThemeData(
backgroundColor: AppColors.surfaceVariant,
selectedColor: AppColors.primaryContainer,
disabledColor: AppColors.surfaceVariant.withValues(alpha: 0.5),
labelStyle: AppTextStyles.labelMedium,
secondaryLabelStyle: AppTextStyles.labelMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.chipRadius),
),
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.spacingSm,
vertical: AppDimensions.spacingXs,
),
),
// 对话框主题
dialogTheme: DialogThemeData(
backgroundColor: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.dialogRadius),
),
titleTextStyle: AppTextStyles.headlineSmall,
contentTextStyle: AppTextStyles.bodyMedium,
),
// 提示条主题
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.inverseSurface,
contentTextStyle: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onInverseSurface,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.radiusSm),
),
behavior: SnackBarBehavior.floating,
),
// 浮动操作按钮主题
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
elevation: 6,
focusElevation: 8,
hoverElevation: 8,
highlightElevation: 12,
),
// 分割线主题
dividerTheme: const DividerThemeData(
color: AppColors.outline,
thickness: AppDimensions.dividerHeight,
space: AppDimensions.dividerHeight,
),
// 列表项主题
listTileTheme: ListTileThemeData(
contentPadding: const EdgeInsets.symmetric(
horizontal: AppDimensions.listItemPadding,
vertical: AppDimensions.spacingXs,
),
titleTextStyle: AppTextStyles.listTitle,
subtitleTextStyle: AppTextStyles.listSubtitle,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.radiusSm),
),
),
// 开关主题
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.onPrimary;
}
return AppColors.outline;
}),
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primary;
}
return AppColors.surfaceVariant;
}),
),
// 复选框主题
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primary;
}
return Colors.transparent;
}),
checkColor: WidgetStateProperty.all(AppColors.onPrimary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.radiusXs),
),
),
// 单选按钮主题
radioTheme: RadioThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primary;
}
return AppColors.onSurfaceVariant;
}),
),
// 滑块主题
sliderTheme: const SliderThemeData(
activeTrackColor: AppColors.primary,
inactiveTrackColor: AppColors.surfaceVariant,
thumbColor: AppColors.primary,
overlayColor: AppColors.primaryContainer,
valueIndicatorColor: AppColors.primary,
),
// 进度指示器主题
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primary,
linearTrackColor: AppColors.surfaceVariant,
circularTrackColor: AppColors.surfaceVariant,
),
);
}
// ============ 深色主题 ============
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
// 禁用Google Fonts使用系统默认字体
fontFamily: null, // 不指定字体,使用系统默认
// 颜色方案
colorScheme: const ColorScheme.dark(
primary: AppColors.primaryDark,
onPrimary: AppColors.onPrimaryDark,
primaryContainer: AppColors.primaryContainerDark,
onPrimaryContainer: AppColors.onPrimaryContainerDark,
secondary: AppColors.secondaryDark,
onSecondary: AppColors.onSecondaryDark,
secondaryContainer: AppColors.secondaryContainerDark,
onSecondaryContainer: AppColors.onSecondaryContainerDark,
tertiary: AppColors.tertiaryDark,
onTertiary: AppColors.onTertiaryDark,
tertiaryContainer: AppColors.tertiaryContainerDark,
onTertiaryContainer: AppColors.onTertiaryContainerDark,
error: AppColors.errorDark,
onError: AppColors.onErrorDark,
errorContainer: AppColors.errorContainerDark,
onErrorContainer: AppColors.onErrorContainerDark,
surface: AppColors.surfaceDark,
onSurface: AppColors.onSurfaceDark,
surfaceContainerHighest: AppColors.surfaceVariantDark,
onSurfaceVariant: AppColors.onSurfaceVariantDark,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
shadow: AppColors.shadowDark,
scrim: AppColors.scrimDark,
inverseSurface: AppColors.inverseSurfaceDark,
onInverseSurface: AppColors.onInverseSurfaceDark,
inversePrimary: AppColors.inversePrimaryDark,
surfaceTint: AppColors.surfaceTintDark,
),
// 文本主题(深色适配)
textTheme: AppTextStyles.textTheme.apply(
bodyColor: AppColors.onSurfaceDark,
displayColor: AppColors.onSurfaceDark,
),
// AppBar主题
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surfaceDark,
foregroundColor: AppColors.onSurfaceDark,
elevation: 0,
scrolledUnderElevation: 1,
centerTitle: true,
titleTextStyle: AppTextStyles.titleLarge.copyWith(
color: AppColors.onSurfaceDark,
),
toolbarHeight: AppDimensions.appBarHeight,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
),
// 其他组件主题配置与亮色主题类似,但使用深色颜色
// 为了简洁,这里省略了重复的配置
// 实际项目中应该完整配置所有组件的深色主题
);
}
// ============ 主题扩展 ============
/// 获取当前主题的学习模块颜色
static Map<String, Color> getLearningColors(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return {
'vocabulary': isDark ? AppColors.vocabularyDark : AppColors.vocabulary,
'listening': isDark ? AppColors.listeningDark : AppColors.listening,
'reading': isDark ? AppColors.readingDark : AppColors.reading,
'writing': isDark ? AppColors.writingDark : AppColors.writing,
'speaking': isDark ? AppColors.speakingDark : AppColors.speaking,
};
}
/// 获取等级颜色
static Color getLevelColor(String level, {bool isDark = false}) {
switch (level.toLowerCase()) {
case 'beginner':
return isDark ? AppColors.beginnerDark : AppColors.beginner;
case 'intermediate':
return isDark ? AppColors.intermediateDark : AppColors.intermediate;
case 'advanced':
return isDark ? AppColors.advancedDark : AppColors.advanced;
default:
return isDark ? AppColors.primaryDark : AppColors.primary;
}
}
/// 获取进度颜色
static Color getProgressColor(double progress, {bool isDark = false}) {
if (progress < 0.3) {
return isDark ? AppColors.progressLowDark : AppColors.progressLow;
} else if (progress < 0.7) {
return isDark ? AppColors.progressMediumDark : AppColors.progressMedium;
} else {
return isDark ? AppColors.progressHighDark : AppColors.progressHigh;
}
}
}

View File

@@ -0,0 +1,141 @@
/// 应用异常基类
abstract class AppException implements Exception {
final String message;
final String? code;
final dynamic details;
const AppException(this.message, {this.code, this.details});
@override
String toString() => 'AppException: $message';
}
/// 网络异常
class NetworkException extends AppException {
const NetworkException(super.message, {super.code, super.details});
@override
String toString() => 'NetworkException: $message';
}
/// 认证异常
class AuthException extends AppException {
const AuthException(super.message, {super.code, super.details});
@override
String toString() => 'AuthException: $message';
}
/// 验证异常
class ValidationException extends AppException {
const ValidationException(super.message, {super.code, super.details});
@override
String toString() => 'ValidationException: $message';
}
/// 服务器异常
class ServerException extends AppException {
const ServerException(super.message, {super.code, super.details});
@override
String toString() => 'ServerException: $message';
}
/// 缓存异常
class CacheException extends AppException {
const CacheException(super.message, {super.code, super.details});
@override
String toString() => 'CacheException: $message';
}
/// 文件异常
class FileException extends AppException {
const FileException(super.message, {super.code, super.details});
@override
String toString() => 'FileException: $message';
}
/// 权限异常
class PermissionException extends AppException {
const PermissionException(super.message, {super.code, super.details});
@override
String toString() => 'PermissionException: $message';
}
/// 业务逻辑异常
class BusinessException extends AppException {
const BusinessException(super.message, {super.code, super.details});
@override
String toString() => 'BusinessException: $message';
}
/// 超时异常
class TimeoutException extends AppException {
const TimeoutException(super.message, {super.code, super.details});
@override
String toString() => 'TimeoutException: $message';
}
/// 数据解析异常
class ParseException extends AppException {
const ParseException(super.message, {super.code, super.details});
@override
String toString() => 'ParseException: $message';
}
/// 通用应用异常
class GeneralAppException extends AppException {
const GeneralAppException(super.message, {super.code, super.details});
@override
String toString() => 'GeneralAppException: $message';
}
/// 异常处理工具类
class ExceptionHandler {
/// 处理异常并返回用户友好的错误信息
static String getErrorMessage(dynamic error) {
if (error is AppException) {
return error.message;
} else if (error is Exception) {
return '发生了未知错误,请稍后重试';
} else {
return '系统错误,请联系客服';
}
}
/// 记录异常
static void logException(dynamic error, {StackTrace? stackTrace}) {
// 这里可以集成日志记录服务如Firebase Crashlytics
print('Exception: $error');
if (stackTrace != null) {
print('StackTrace: $stackTrace');
}
}
/// 判断是否为网络相关异常
static bool isNetworkError(dynamic error) {
return error is NetworkException ||
error is TimeoutException ||
(error is AppException && error.code?.contains('network') == true);
}
/// 判断是否为认证相关异常
static bool isAuthError(dynamic error) {
return error is AuthException ||
(error is AppException && error.code?.contains('auth') == true);
}
/// 判断是否为验证相关异常
static bool isValidationError(dynamic error) {
return error is ValidationException ||
(error is AppException && error.code?.contains('validation') == true);
}
}

View File

@@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
/// 全局消息提示工具类
///
/// 特性:
/// - 新消息自动取消之前的提示
/// - 统一的样式和动画
/// - 缩短显示时间,避免堆积
class MessageUtils {
MessageUtils._();
/// 显示普通消息
static void showMessage(
BuildContext context,
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),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 显示成功消息
static void showSuccess(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.green,
duration: duration,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 显示错误消息
static void showError(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.red,
duration: duration,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 显示警告消息
static void showWarning(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.orange,
duration: duration,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 显示信息消息
static void showInfo(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.info, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.blue,
duration: duration,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 显示加载消息(不自动消失)
static void showLoading(
BuildContext context,
String message,
) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
duration: const Duration(days: 1), // 长时间显示
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(16),
),
);
}
/// 隐藏所有消息
static void hideAll(BuildContext context) {
ScaffoldMessenger.of(context).clearSnackBars();
}
}

View File

@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
/// 响应式布局工具类
class ResponsiveUtils {
// 断点定义
static const double mobileBreakpoint = 600;
static const double tabletBreakpoint = 900;
static const double desktopBreakpoint = 1200;
/// 判断是否为移动端
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < mobileBreakpoint;
}
/// 判断是否为平板端
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= mobileBreakpoint && width < desktopBreakpoint;
}
/// 判断是否为桌面端
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopBreakpoint;
}
/// 获取网格列数
static int getGridColumns(BuildContext context, {
int mobileColumns = 1,
int tabletColumns = 2,
int desktopColumns = 3,
}) {
if (isMobile(context)) return mobileColumns;
if (isTablet(context)) return tabletColumns;
return desktopColumns;
}
/// 获取响应式边距
static EdgeInsets getResponsivePadding(BuildContext context, {
EdgeInsets? mobile,
EdgeInsets? tablet,
EdgeInsets? desktop,
}) {
if (isMobile(context)) return mobile ?? const EdgeInsets.all(16);
if (isTablet(context)) return tablet ?? const EdgeInsets.all(20);
return desktop ?? const EdgeInsets.all(24);
}
/// 获取响应式字体大小
static double getResponsiveFontSize(BuildContext context, {
double mobileSize = 14,
double tabletSize = 16,
double desktopSize = 18,
}) {
if (isMobile(context)) return mobileSize;
if (isTablet(context)) return tabletSize;
return desktopSize;
}
/// 获取分类卡片宽度
static double getCategoryCardWidth(BuildContext context) {
if (isMobile(context)) return 80;
if (isTablet(context)) return 100;
return 120;
}
/// 获取分类卡片高度
static double getCategoryCardHeight(BuildContext context) {
if (isMobile(context)) return 90;
if (isTablet(context)) return 100;
return 110;
}
/// 根据屏幕宽度返回不同的值
static T getValueForScreenSize<T>(
BuildContext context, {
required T mobile,
T? tablet,
T? desktop,
}) {
if (isMobile(context)) return mobile;
if (isTablet(context)) return tablet ?? mobile;
return desktop ?? tablet ?? mobile;
}
}
/// 响应式布局构建器
class ResponsiveBuilder extends StatelessWidget {
final Widget Function(BuildContext context, bool isMobile, bool isTablet, bool isDesktop) builder;
const ResponsiveBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
final isMobile = ResponsiveUtils.isMobile(context);
final isTablet = ResponsiveUtils.isTablet(context);
final isDesktop = ResponsiveUtils.isDesktop(context);
return builder(context, isMobile, isTablet, isDesktop);
}
}
/// 响应式网格视图
class ResponsiveGridView extends StatelessWidget {
final List<Widget> children;
final int mobileColumns;
final int tabletColumns;
final int desktopColumns;
final double spacing;
final EdgeInsets padding;
final double childAspectRatio;
const ResponsiveGridView({
super.key,
required this.children,
this.mobileColumns = 1,
this.tabletColumns = 2,
this.desktopColumns = 3,
this.spacing = 16,
this.padding = const EdgeInsets.all(16),
this.childAspectRatio = 1.0,
});
@override
Widget build(BuildContext context) {
final columns = ResponsiveUtils.getGridColumns(
context,
mobileColumns: mobileColumns,
tabletColumns: tabletColumns,
desktopColumns: desktopColumns,
);
return Padding(
padding: padding,
child: GridView.count(
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: childAspectRatio,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: children,
),
);
}
}

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 自定义按钮组件
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isOutlined;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
final double? width;
final double? height;
final EdgeInsetsGeometry? padding;
const CustomButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isOutlined = false,
this.backgroundColor,
this.textColor,
this.icon,
this.width,
this.height,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveBackgroundColor = backgroundColor ??
(isOutlined ? Colors.transparent : AppColors.primary);
final effectiveTextColor = textColor ??
(isOutlined ? AppColors.primary : AppColors.onPrimary);
final effectivePadding = padding ?? EdgeInsets.symmetric(
vertical: AppDimensions.spacingMd,
horizontal: AppDimensions.spacingLg,
);
Widget buttonChild = isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveTextColor),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(
icon,
color: effectiveTextColor,
size: 20,
),
SizedBox(width: AppDimensions.spacingSm),
],
Text(
text,
style: AppTextStyles.bodyLarge.copyWith(
color: effectiveTextColor,
fontWeight: FontWeight.w600,
),
),
],
);
if (isOutlined) {
return SizedBox(
width: width,
height: height,
child: OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
side: BorderSide(
color: onPressed != null ? AppColors.primary : AppColors.onSurface.withOpacity(0.3),
width: 1.5,
),
padding: effectivePadding,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: buttonChild,
),
);
}
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: effectiveBackgroundColor,
foregroundColor: effectiveTextColor,
padding: effectivePadding,
elevation: 2,
shadowColor: AppColors.primary.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
disabledBackgroundColor: AppColors.onSurface.withOpacity(0.12),
disabledForegroundColor: AppColors.onSurface.withOpacity(0.38),
),
child: buttonChild,
),
);
}
}
/// 图标按钮组件
class CustomIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback? onPressed;
final Color? backgroundColor;
final Color? iconColor;
final double? size;
final String? tooltip;
final bool isLoading;
const CustomIconButton({
super.key,
required this.icon,
this.onPressed,
this.backgroundColor,
this.iconColor,
this.size,
this.tooltip,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
final effectiveSize = size ?? 48.0;
final effectiveBackgroundColor = backgroundColor ?? AppColors.primary;
final effectiveIconColor = iconColor ?? AppColors.onPrimary;
Widget iconWidget = isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveIconColor),
),
)
: Icon(
icon,
color: effectiveIconColor,
size: effectiveSize * 0.5,
);
Widget button = Container(
width: effectiveSize,
height: effectiveSize,
decoration: BoxDecoration(
color: effectiveBackgroundColor,
borderRadius: BorderRadius.circular(effectiveSize / 2),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(effectiveSize / 2),
child: Center(child: iconWidget),
),
),
);
if (tooltip != null) {
return Tooltip(
message: tooltip!,
child: button,
);
}
return button;
}
}

View File

@@ -0,0 +1,265 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 自定义文本输入框组件
class CustomTextField extends StatefulWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final String? helperText;
final String? errorText;
final IconData? prefixIcon;
final Widget? suffixIcon;
final bool obscureText;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final int? maxLines;
final int? minLines;
final int? maxLength;
final bool enabled;
final bool readOnly;
final bool autofocus;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function()? onTap;
final void Function(String)? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final EdgeInsetsGeometry? contentPadding;
final TextStyle? textStyle;
final TextStyle? labelStyle;
final TextStyle? hintStyle;
final Color? fillColor;
final Color? borderColor;
final Color? focusedBorderColor;
final Color? errorBorderColor;
final double? borderRadius;
final bool filled;
const CustomTextField({
super.key,
this.controller,
this.labelText,
this.hintText,
this.helperText,
this.errorText,
this.prefixIcon,
this.suffixIcon,
this.obscureText = false,
this.keyboardType,
this.textInputAction,
this.maxLines = 1,
this.minLines,
this.maxLength,
this.enabled = true,
this.readOnly = false,
this.autofocus = false,
this.validator,
this.onChanged,
this.onTap,
this.onSubmitted,
this.inputFormatters,
this.focusNode,
this.contentPadding,
this.textStyle,
this.labelStyle,
this.hintStyle,
this.fillColor,
this.borderColor,
this.focusedBorderColor,
this.errorBorderColor,
this.borderRadius,
this.filled = true,
});
@override
State<CustomTextField> createState() => _CustomTextFieldState();
}
class _CustomTextFieldState extends State<CustomTextField> {
late FocusNode _focusNode;
bool _isFocused = false;
@override
void initState() {
super.initState();
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onFocusChange);
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
} else {
_focusNode.removeListener(_onFocusChange);
}
super.dispose();
}
void _onFocusChange() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
}
@override
Widget build(BuildContext context) {
final effectiveBorderRadius = widget.borderRadius ?? 8.0;
final effectiveFillColor = widget.fillColor ?? AppColors.surface;
final effectiveBorderColor = widget.borderColor ?? AppColors.onSurface.withOpacity(0.3);
final effectiveFocusedBorderColor = widget.focusedBorderColor ?? AppColors.primary;
final effectiveErrorBorderColor = widget.errorBorderColor ?? AppColors.error;
final effectiveContentPadding = widget.contentPadding ?? EdgeInsets.symmetric(
horizontal: AppDimensions.spacingMd,
vertical: AppDimensions.spacingMd,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.labelText != null) ...[
Text(
widget.labelText!,
style: widget.labelStyle ?? AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: AppDimensions.spacingSm),
],
TextFormField(
controller: widget.controller,
focusNode: _focusNode,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
maxLines: widget.maxLines,
minLines: widget.minLines,
maxLength: widget.maxLength,
enabled: widget.enabled,
readOnly: widget.readOnly,
autofocus: widget.autofocus,
validator: widget.validator,
onChanged: widget.onChanged,
onTap: widget.onTap,
onFieldSubmitted: widget.onSubmitted,
inputFormatters: widget.inputFormatters,
style: widget.textStyle ?? AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: widget.hintStyle ?? AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface.withOpacity(0.6),
),
helperText: widget.helperText,
errorText: widget.errorText,
prefixIcon: widget.prefixIcon != null
? Icon(
widget.prefixIcon,
color: _isFocused ? effectiveFocusedBorderColor : AppColors.onSurface.withOpacity(0.6),
)
: null,
suffixIcon: widget.suffixIcon,
filled: widget.filled,
fillColor: effectiveFillColor,
contentPadding: effectiveContentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveBorderColor,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveBorderColor,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveFocusedBorderColor,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveErrorBorderColor,
width: 1.0,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: effectiveErrorBorderColor,
width: 2.0,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
borderSide: BorderSide(
color: AppColors.onSurface.withOpacity(0.12),
width: 1.0,
),
),
),
),
],
);
}
}
/// 搜索输入框组件
class SearchTextField extends StatelessWidget {
final TextEditingController? controller;
final String? hintText;
final void Function(String)? onChanged;
final void Function(String)? onSubmitted;
final VoidCallback? onClear;
final bool autofocus;
final FocusNode? focusNode;
const SearchTextField({
super.key,
this.controller,
this.hintText,
this.onChanged,
this.onSubmitted,
this.onClear,
this.autofocus = false,
this.focusNode,
});
@override
Widget build(BuildContext context) {
return CustomTextField(
controller: controller,
hintText: hintText ?? '搜索...',
prefixIcon: Icons.search,
suffixIcon: controller?.text.isNotEmpty == true
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller?.clear();
onClear?.call();
},
)
: null,
onChanged: onChanged,
onSubmitted: onSubmitted,
autofocus: autofocus,
focusNode: focusNode,
textInputAction: TextInputAction.search,
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_dimensions.dart';
/// 404页面 - 页面未找到
class NotFoundScreen extends StatelessWidget {
final String? routeName;
const NotFoundScreen({
super.key,
this.routeName,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('页面未找到'),
backgroundColor: AppColors.surface,
foregroundColor: AppColors.onSurface,
),
body: Center(
child: Padding(
padding: EdgeInsets.all(AppDimensions.pagePadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 404图标
Icon(
Icons.error_outline,
size: AppDimensions.iconXxl * 2,
color: AppColors.outline,
),
SizedBox(height: AppDimensions.spacingXl),
// 标题
Text(
'页面未找到',
style: AppTextStyles.headlineLarge.copyWith(
color: AppColors.onSurface,
),
textAlign: TextAlign.center,
),
SizedBox(height: AppDimensions.spacingMd),
// 描述
Text(
routeName != null
? '路由 "$routeName" 不存在'
: '您访问的页面不存在或已被移除',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
SizedBox(height: AppDimensions.spacingXxl),
// 返回按钮
ElevatedButton.icon(
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
Navigator.of(context).pushReplacementNamed('/');
}
},
icon: const Icon(Icons.arrow_back),
label: const Text('返回'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
padding: EdgeInsets.symmetric(
horizontal: AppDimensions.spacingXl,
vertical: AppDimensions.spacingMd,
),
),
),
SizedBox(height: AppDimensions.spacingMd),
// 回到首页按钮
TextButton(
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/',
(route) => false,
);
},
child: Text(
'回到首页',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
),
),
),
],
),
),
),
);
}
}