This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
/// API响应基础模型
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final String? error;
final int? code;
final Map<String, dynamic>? meta;
const ApiResponse({
required this.success,
required this.message,
this.data,
this.error,
this.code,
this.meta,
});
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse<T>(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
data: json['data'] != null && fromJsonT != null
? fromJsonT(json['data'])
: json['data'] as T?,
error: json['error'] as String?,
code: json['code'] as int?,
meta: json['meta'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data,
'error': error,
'code': code,
'meta': meta,
};
}
/// 成功响应
factory ApiResponse.success({
required String message,
T? data,
Map<String, dynamic>? meta,
}) {
return ApiResponse<T>(
success: true,
message: message,
data: data,
meta: meta,
);
}
/// 失败响应
factory ApiResponse.failure({
required String message,
String? error,
int? code,
Map<String, dynamic>? meta,
}) {
return ApiResponse<T>(
success: false,
message: message,
error: error,
code: code,
meta: meta,
);
}
/// 是否成功
bool get isSuccess => success;
/// 是否失败
bool get isFailure => !success;
@override
String toString() {
return 'ApiResponse(success: $success, message: $message, data: $data, error: $error)';
}
}
/// 分页响应模型
class PaginatedResponse<T> {
final List<T> data;
final PaginationMeta pagination;
const PaginatedResponse({
required this.data,
required this.pagination,
});
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
return PaginatedResponse<T>(
data: (json['data'] as List)
.map((e) => fromJsonT(e as Map<String, dynamic>))
.toList(),
pagination: PaginationMeta.fromJson(json['pagination'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
return {
'data': data,
'pagination': pagination.toJson(),
};
}
}
/// 分页元数据模型
class PaginationMeta {
final int currentPage;
final int totalPages;
final int totalItems;
final int itemsPerPage;
final bool hasNextPage;
final bool hasPreviousPage;
const PaginationMeta({
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.itemsPerPage,
required this.hasNextPage,
required this.hasPreviousPage,
});
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
return PaginationMeta(
currentPage: json['current_page'] as int,
totalPages: json['total_pages'] as int,
totalItems: json['total_items'] as int,
itemsPerPage: json['items_per_page'] as int,
hasNextPage: json['has_next_page'] as bool,
hasPreviousPage: json['has_previous_page'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'current_page': currentPage,
'total_pages': totalPages,
'total_items': totalItems,
'items_per_page': itemsPerPage,
'has_next_page': hasNextPage,
'has_previous_page': hasPreviousPage,
};
}
}
/// 认证响应模型
class AuthResponse {
final String accessToken;
final String refreshToken;
final String tokenType;
final int expiresIn;
final Map<String, dynamic>? user;
const AuthResponse({
required this.accessToken,
required this.refreshToken,
required this.tokenType,
required this.expiresIn,
this.user,
});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
tokenType: json['token_type'] as String? ?? 'Bearer',
expiresIn: json['expires_in'] as int,
user: json['user'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'refresh_token': refreshToken,
'token_type': tokenType,
'expires_in': expiresIn,
'user': user,
};
}
}
/// 错误响应模型
class ErrorResponse {
final String message;
final String? code;
final List<ValidationError>? validationErrors;
final Map<String, dynamic>? details;
const ErrorResponse({
required this.message,
this.code,
this.validationErrors,
this.details,
});
factory ErrorResponse.fromJson(Map<String, dynamic> json) {
return ErrorResponse(
message: json['message'] as String,
code: json['code'] as String?,
validationErrors: json['validation_errors'] != null
? (json['validation_errors'] as List)
.map((e) => ValidationError.fromJson(e as Map<String, dynamic>))
.toList()
: null,
details: json['details'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'message': message,
'code': code,
'validation_errors': validationErrors?.map((e) => e.toJson()).toList(),
'details': details,
};
}
}
/// 验证错误模型
class ValidationError {
final String field;
final String message;
final String? code;
const ValidationError({
required this.field,
required this.message,
this.code,
});
factory ValidationError.fromJson(Map<String, dynamic> json) {
return ValidationError(
field: json['field'] as String,
message: json['message'] as String,
code: json['code'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'field': field,
'message': message,
'code': code,
};
}
}

View File

@@ -0,0 +1,90 @@
import 'package:json_annotation/json_annotation.dart';
part 'notification_model.g.dart';
/// 通知模型
@JsonSerializable()
class NotificationModel {
final int id;
@JsonKey(name: 'user_id')
final int userId;
final String type;
final String title;
final String content;
final String? link;
@JsonKey(name: 'is_read')
final bool isRead;
final int priority;
@JsonKey(name: 'created_at')
final DateTime createdAt;
@JsonKey(name: 'read_at')
final DateTime? readAt;
const NotificationModel({
required this.id,
required this.userId,
required this.type,
required this.title,
required this.content,
this.link,
required this.isRead,
required this.priority,
required this.createdAt,
this.readAt,
});
factory NotificationModel.fromJson(Map<String, dynamic> json) =>
_$NotificationModelFromJson(json);
Map<String, dynamic> toJson() => _$NotificationModelToJson(this);
/// 通知类型
String get typeLabel {
switch (type) {
case 'system':
return '系统通知';
case 'learning':
return '学习提醒';
case 'achievement':
return '成就通知';
default:
return '通知';
}
}
/// 优先级
String get priorityLabel {
switch (priority) {
case 0:
return '普通';
case 1:
return '重要';
case 2:
return '紧急';
default:
return '普通';
}
}
/// 格式化时间
String get timeAgo {
final now = DateTime.now();
final difference = now.difference(createdAt);
if (difference.inDays > 0) {
return '${difference.inDays}天前';
} else if (difference.inHours > 0) {
return '${difference.inHours}小时前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分钟前';
} else {
return '刚刚';
}
}
}

View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationModel _$NotificationModelFromJson(Map<String, dynamic> json) =>
NotificationModel(
id: (json['id'] as num).toInt(),
userId: (json['user_id'] as num).toInt(),
type: json['type'] as String,
title: json['title'] as String,
content: json['content'] as String,
link: json['link'] as String?,
isRead: json['is_read'] as bool,
priority: (json['priority'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
readAt: json['read_at'] == null
? null
: DateTime.parse(json['read_at'] as String),
);
Map<String, dynamic> _$NotificationModelToJson(NotificationModel instance) =>
<String, dynamic>{
'id': instance.id,
'user_id': instance.userId,
'type': instance.type,
'title': instance.title,
'content': instance.content,
'link': instance.link,
'is_read': instance.isRead,
'priority': instance.priority,
'created_at': instance.createdAt.toIso8601String(),
'read_at': instance.readAt?.toIso8601String(),
};

View File

@@ -0,0 +1,220 @@
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
/// 用户模型
@JsonSerializable()
class UserModel {
@JsonKey(name: 'user_id')
final int userId;
final String username;
final String email;
final String? nickname;
final String? avatar;
final String? phone;
final DateTime? birthday;
final String? gender;
final String? bio;
@JsonKey(name: 'learning_level')
final String learningLevel;
@JsonKey(name: 'target_language')
final String targetLanguage;
@JsonKey(name: 'native_language')
final String nativeLanguage;
@JsonKey(name: 'daily_goal')
final int dailyGoal;
@JsonKey(name: 'study_streak')
final int studyStreak;
@JsonKey(name: 'total_study_days')
final int totalStudyDays;
@JsonKey(name: 'vocabulary_count')
final int vocabularyCount;
@JsonKey(name: 'experience_points')
final int experiencePoints;
@JsonKey(name: 'current_level')
final int currentLevel;
@JsonKey(name: 'created_at')
final DateTime createdAt;
@JsonKey(name: 'updated_at')
final DateTime updatedAt;
@JsonKey(name: 'last_login_at')
final DateTime? lastLoginAt;
@JsonKey(name: 'is_premium')
final bool isPremium;
@JsonKey(name: 'premium_expires_at')
final DateTime? premiumExpiresAt;
const UserModel({
required this.userId,
required this.username,
required this.email,
this.nickname,
this.avatar,
this.phone,
this.birthday,
this.gender,
this.bio,
required this.learningLevel,
required this.targetLanguage,
required this.nativeLanguage,
required this.dailyGoal,
required this.studyStreak,
required this.totalStudyDays,
required this.vocabularyCount,
required this.experiencePoints,
required this.currentLevel,
required this.createdAt,
required this.updatedAt,
this.lastLoginAt,
required this.isPremium,
this.premiumExpiresAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
UserModel copyWith({
int? userId,
String? username,
String? email,
String? nickname,
String? avatar,
String? phone,
DateTime? birthday,
String? gender,
String? bio,
String? learningLevel,
String? targetLanguage,
String? nativeLanguage,
int? dailyGoal,
int? studyStreak,
int? totalStudyDays,
int? vocabularyCount,
int? experiencePoints,
int? currentLevel,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastLoginAt,
bool? isPremium,
DateTime? premiumExpiresAt,
}) {
return UserModel(
userId: userId ?? this.userId,
username: username ?? this.username,
email: email ?? this.email,
nickname: nickname ?? this.nickname,
avatar: avatar ?? this.avatar,
phone: phone ?? this.phone,
birthday: birthday ?? this.birthday,
gender: gender ?? this.gender,
bio: bio ?? this.bio,
learningLevel: learningLevel ?? this.learningLevel,
targetLanguage: targetLanguage ?? this.targetLanguage,
nativeLanguage: nativeLanguage ?? this.nativeLanguage,
dailyGoal: dailyGoal ?? this.dailyGoal,
studyStreak: studyStreak ?? this.studyStreak,
totalStudyDays: totalStudyDays ?? this.totalStudyDays,
vocabularyCount: vocabularyCount ?? this.vocabularyCount,
experiencePoints: experiencePoints ?? this.experiencePoints,
currentLevel: currentLevel ?? this.currentLevel,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
isPremium: isPremium ?? this.isPremium,
premiumExpiresAt: premiumExpiresAt ?? this.premiumExpiresAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserModel &&
other.userId == userId &&
other.username == username &&
other.email == email;
}
@override
int get hashCode {
return userId.hashCode ^
username.hashCode ^
email.hashCode;
}
@override
String toString() {
return 'UserModel(userId: $userId, username: $username, email: $email)';
}
}
/// 用户统计信息模型
@JsonSerializable()
class UserStatsModel {
@JsonKey(name: 'total_words_learned')
final int totalWordsLearned;
@JsonKey(name: 'words_learned_today')
final int wordsLearnedToday;
@JsonKey(name: 'study_time_today')
final int studyTimeToday; // 分钟
@JsonKey(name: 'total_study_time')
final int totalStudyTime; // 分钟
@JsonKey(name: 'listening_score')
final double listeningScore;
@JsonKey(name: 'reading_score')
final double readingScore;
@JsonKey(name: 'writing_score')
final double writingScore;
@JsonKey(name: 'speaking_score')
final double speakingScore;
@JsonKey(name: 'overall_score')
final double overallScore;
@JsonKey(name: 'weekly_progress')
final List<int> weeklyProgress;
@JsonKey(name: 'monthly_progress')
final List<int> monthlyProgress;
const UserStatsModel({
required this.totalWordsLearned,
required this.wordsLearnedToday,
required this.studyTimeToday,
required this.totalStudyTime,
required this.listeningScore,
required this.readingScore,
required this.writingScore,
required this.speakingScore,
required this.overallScore,
required this.weeklyProgress,
required this.monthlyProgress,
});
factory UserStatsModel.fromJson(Map<String, dynamic> json) => _$UserStatsModelFromJson(json);
Map<String, dynamic> toJson() => _$UserStatsModelToJson(this);
}

View File

@@ -0,0 +1,99 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
userId: (json['user_id'] as num).toInt(),
username: json['username'] as String,
email: json['email'] as String,
nickname: json['nickname'] as String?,
avatar: json['avatar'] as String?,
phone: json['phone'] as String?,
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
gender: json['gender'] as String?,
bio: json['bio'] as String?,
learningLevel: json['learning_level'] as String,
targetLanguage: json['target_language'] as String,
nativeLanguage: json['native_language'] as String,
dailyGoal: (json['daily_goal'] as num).toInt(),
studyStreak: (json['study_streak'] as num).toInt(),
totalStudyDays: (json['total_study_days'] as num).toInt(),
vocabularyCount: (json['vocabulary_count'] as num).toInt(),
experiencePoints: (json['experience_points'] as num).toInt(),
currentLevel: (json['current_level'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
lastLoginAt: json['last_login_at'] == null
? null
: DateTime.parse(json['last_login_at'] as String),
isPremium: json['is_premium'] as bool,
premiumExpiresAt: json['premium_expires_at'] == null
? null
: DateTime.parse(json['premium_expires_at'] as String),
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'user_id': instance.userId,
'username': instance.username,
'email': instance.email,
'nickname': instance.nickname,
'avatar': instance.avatar,
'phone': instance.phone,
'birthday': instance.birthday?.toIso8601String(),
'gender': instance.gender,
'bio': instance.bio,
'learning_level': instance.learningLevel,
'target_language': instance.targetLanguage,
'native_language': instance.nativeLanguage,
'daily_goal': instance.dailyGoal,
'study_streak': instance.studyStreak,
'total_study_days': instance.totalStudyDays,
'vocabulary_count': instance.vocabularyCount,
'experience_points': instance.experiencePoints,
'current_level': instance.currentLevel,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'last_login_at': instance.lastLoginAt?.toIso8601String(),
'is_premium': instance.isPremium,
'premium_expires_at': instance.premiumExpiresAt?.toIso8601String(),
};
UserStatsModel _$UserStatsModelFromJson(Map<String, dynamic> json) =>
UserStatsModel(
totalWordsLearned: (json['total_words_learned'] as num).toInt(),
wordsLearnedToday: (json['words_learned_today'] as num).toInt(),
studyTimeToday: (json['study_time_today'] as num).toInt(),
totalStudyTime: (json['total_study_time'] as num).toInt(),
listeningScore: (json['listening_score'] as num).toDouble(),
readingScore: (json['reading_score'] as num).toDouble(),
writingScore: (json['writing_score'] as num).toDouble(),
speakingScore: (json['speaking_score'] as num).toDouble(),
overallScore: (json['overall_score'] as num).toDouble(),
weeklyProgress: (json['weekly_progress'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
monthlyProgress: (json['monthly_progress'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$UserStatsModelToJson(UserStatsModel instance) =>
<String, dynamic>{
'total_words_learned': instance.totalWordsLearned,
'words_learned_today': instance.wordsLearnedToday,
'study_time_today': instance.studyTimeToday,
'total_study_time': instance.totalStudyTime,
'listening_score': instance.listeningScore,
'reading_score': instance.readingScore,
'writing_score': instance.writingScore,
'speaking_score': instance.speakingScore,
'overall_score': instance.overallScore,
'weekly_progress': instance.weeklyProgress,
'monthly_progress': instance.monthlyProgress,
};

View File

@@ -0,0 +1,292 @@
/// 词汇模型
class VocabularyModel {
final int wordId;
final String word;
final String pronunciation;
final String phonetic;
final List<WordMeaning> meanings;
final String? etymology;
final List<String> examples;
final String? imageUrl;
final String? audioUrl;
final int difficulty;
final List<String> tags;
final DateTime createdAt;
final DateTime updatedAt;
const VocabularyModel({
required this.wordId,
required this.word,
required this.pronunciation,
required this.phonetic,
required this.meanings,
this.etymology,
required this.examples,
this.imageUrl,
this.audioUrl,
required this.difficulty,
required this.tags,
required this.createdAt,
required this.updatedAt,
});
factory VocabularyModel.fromJson(Map<String, dynamic> json) {
return VocabularyModel(
wordId: json['word_id'] as int,
word: json['word'] as String,
pronunciation: json['pronunciation'] as String,
phonetic: json['phonetic'] as String,
meanings: (json['meanings'] as List)
.map((e) => WordMeaning.fromJson(e as Map<String, dynamic>))
.toList(),
etymology: json['etymology'] as String?,
examples: (json['examples'] as List).cast<String>(),
imageUrl: json['image_url'] as String?,
audioUrl: json['audio_url'] as String?,
difficulty: json['difficulty'] as int,
tags: (json['tags'] as List).cast<String>(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'word_id': wordId,
'word': word,
'pronunciation': pronunciation,
'phonetic': phonetic,
'meanings': meanings.map((e) => e.toJson()).toList(),
'etymology': etymology,
'examples': examples,
'image_url': imageUrl,
'audio_url': audioUrl,
'difficulty': difficulty,
'tags': tags,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
VocabularyModel copyWith({
int? wordId,
String? word,
String? pronunciation,
String? phonetic,
List<WordMeaning>? meanings,
String? etymology,
List<String>? examples,
String? imageUrl,
String? audioUrl,
int? difficulty,
List<String>? tags,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return VocabularyModel(
wordId: wordId ?? this.wordId,
word: word ?? this.word,
pronunciation: pronunciation ?? this.pronunciation,
phonetic: phonetic ?? this.phonetic,
meanings: meanings ?? this.meanings,
etymology: etymology ?? this.etymology,
examples: examples ?? this.examples,
imageUrl: imageUrl ?? this.imageUrl,
audioUrl: audioUrl ?? this.audioUrl,
difficulty: difficulty ?? this.difficulty,
tags: tags ?? this.tags,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
/// 词汇含义模型
class WordMeaning {
final String partOfSpeech;
final String definition;
final String? chineseDefinition;
final List<String> synonyms;
final List<String> antonyms;
final List<String> examples;
const WordMeaning({
required this.partOfSpeech,
required this.definition,
this.chineseDefinition,
required this.synonyms,
required this.antonyms,
required this.examples,
});
factory WordMeaning.fromJson(Map<String, dynamic> json) {
return WordMeaning(
partOfSpeech: json['part_of_speech'] as String,
definition: json['definition'] as String,
chineseDefinition: json['chinese_definition'] as String?,
synonyms: (json['synonyms'] as List).cast<String>(),
antonyms: (json['antonyms'] as List).cast<String>(),
examples: (json['examples'] as List).cast<String>(),
);
}
Map<String, dynamic> toJson() {
return {
'part_of_speech': partOfSpeech,
'definition': definition,
'chinese_definition': chineseDefinition,
'synonyms': synonyms,
'antonyms': antonyms,
'examples': examples,
};
}
}
/// 用户词汇学习记录模型
class UserVocabularyModel {
final int userWordId;
final int userId;
final int wordId;
final VocabularyModel? vocabulary;
final LearningStatus status;
final int reviewCount;
final int correctCount;
final int incorrectCount;
final double masteryLevel;
final DateTime? lastReviewAt;
final DateTime? nextReviewAt;
final DateTime createdAt;
final DateTime updatedAt;
const UserVocabularyModel({
required this.userWordId,
required this.userId,
required this.wordId,
this.vocabulary,
required this.status,
required this.reviewCount,
required this.correctCount,
required this.incorrectCount,
required this.masteryLevel,
this.lastReviewAt,
this.nextReviewAt,
required this.createdAt,
required this.updatedAt,
});
factory UserVocabularyModel.fromJson(Map<String, dynamic> json) {
return UserVocabularyModel(
userWordId: json['user_word_id'] as int,
userId: json['user_id'] as int,
wordId: json['word_id'] as int,
vocabulary: json['vocabulary'] != null
? VocabularyModel.fromJson(json['vocabulary'] as Map<String, dynamic>)
: null,
status: LearningStatus.values.firstWhere(
(e) => e.name == json['status'],
orElse: () => LearningStatus.new_word,
),
reviewCount: json['review_count'] as int,
correctCount: json['correct_count'] as int,
incorrectCount: json['incorrect_count'] as int,
masteryLevel: (json['mastery_level'] as num).toDouble(),
lastReviewAt: json['last_review_at'] != null
? DateTime.parse(json['last_review_at'] as String)
: null,
nextReviewAt: json['next_review_at'] != null
? DateTime.parse(json['next_review_at'] as String)
: null,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'user_word_id': userWordId,
'user_id': userId,
'word_id': wordId,
'vocabulary': vocabulary?.toJson(),
'status': status.name,
'review_count': reviewCount,
'correct_count': correctCount,
'incorrect_count': incorrectCount,
'mastery_level': masteryLevel,
'last_review_at': lastReviewAt?.toIso8601String(),
'next_review_at': nextReviewAt?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
}
/// 学习状态枚举
enum LearningStatus {
new_word('new_word', '新单词'),
learning('learning', '学习中'),
reviewing('reviewing', '复习中'),
mastered('mastered', '已掌握'),
forgotten('forgotten', '已遗忘');
const LearningStatus(this.value, this.label);
final String value;
final String label;
}
/// 词库模型
class VocabularyBookModel {
final int bookId;
final String name;
final String description;
final String category;
final String level;
final int totalWords;
final String? coverImage;
final bool isPremium;
final DateTime createdAt;
final DateTime updatedAt;
const VocabularyBookModel({
required this.bookId,
required this.name,
required this.description,
required this.category,
required this.level,
required this.totalWords,
this.coverImage,
required this.isPremium,
required this.createdAt,
required this.updatedAt,
});
factory VocabularyBookModel.fromJson(Map<String, dynamic> json) {
return VocabularyBookModel(
bookId: json['book_id'] as int,
name: json['name'] as String,
description: json['description'] as String,
category: json['category'] as String,
level: json['level'] as String,
totalWords: json['total_words'] as int,
coverImage: json['cover_image'] as String?,
isPremium: json['is_premium'] as bool,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'book_id': bookId,
'name': name,
'description': description,
'category': category,
'level': level,
'total_words': totalWords,
'cover_image': coverImage,
'is_premium': isPremium,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,289 @@
import 'package:flutter/foundation.dart';
import '../models/user_model.dart';
import '../models/api_response.dart';
import '../services/auth_service.dart';
import '../../core/storage/storage_service.dart';
import '../../core/constants/app_constants.dart';
/// 认证状态
enum AuthState {
initial,
loading,
authenticated,
unauthenticated,
error,
}
/// 认证Provider
class AuthProvider extends ChangeNotifier {
final AuthService _authService = AuthService();
AuthState _state = AuthState.initial;
UserModel? _user;
String? _errorMessage;
bool _isLoading = false;
// Getters
AuthState get state => _state;
UserModel? get user => _user;
String? get errorMessage => _errorMessage;
bool get isLoading => _isLoading;
bool get isAuthenticated => _state == AuthState.authenticated && _user != null;
/// 初始化认证状态
Future<void> initialize() async {
_setLoading(true);
try {
if (_authService.isLoggedIn()) {
final cachedUser = _authService.getCachedUser();
if (cachedUser != null) {
_user = cachedUser;
_setState(AuthState.authenticated);
// 尝试刷新用户信息
await _refreshUserInfo();
} else {
_setState(AuthState.unauthenticated);
}
} else {
_setState(AuthState.unauthenticated);
}
} catch (e) {
_setError('初始化失败: $e');
} finally {
_setLoading(false);
}
}
/// 用户注册
Future<bool> register({
required String username,
required String email,
required String password,
required String nickname,
String? phone,
}) async {
_setLoading(true);
_clearError();
try {
final response = await _authService.register(
username: username,
email: email,
password: password,
nickname: nickname,
phone: phone,
);
if (response.success && response.data != null && response.data!.user != null) {
_user = UserModel.fromJson(response.data!.user!);
_setState(AuthState.authenticated);
return true;
} else {
_setError(response.message);
return false;
}
} catch (e) {
_setError('注册失败: $e');
return false;
} finally {
_setLoading(false);
}
}
/// 用户登录
Future<bool> login({
required String account, // 用户名或邮箱
required String password,
bool rememberMe = false,
}) async {
_setLoading(true);
_clearError();
try {
final response = await _authService.login(
account: account,
password: password,
rememberMe: rememberMe,
);
if (response.success && response.data != null && response.data!.user != null) {
_user = UserModel.fromJson(response.data!.user!);
_setState(AuthState.authenticated);
return true;
} else {
_setError(response.message);
return false;
}
} catch (e) {
_setError('登录失败: $e');
return false;
} finally {
_setLoading(false);
}
}
/// 用户登出
Future<void> logout() async {
_setLoading(true);
try {
await _authService.logout();
} catch (e) {
// 即使登出请求失败,也要清除本地状态
debugPrint('Logout error: $e');
} finally {
_user = null;
_setState(AuthState.unauthenticated);
_setLoading(false);
}
}
/// 刷新用户信息
Future<void> _refreshUserInfo() async {
try {
final response = await _authService.getCurrentUser();
if (response.success && response.data != null) {
_user = response.data;
notifyListeners();
}
} catch (e) {
debugPrint('Refresh user info error: $e');
}
}
/// 更新用户信息
Future<bool> updateProfile({
String? nickname,
String? avatar,
String? phone,
DateTime? birthday,
String? gender,
String? bio,
String? learningLevel,
String? targetLanguage,
String? nativeLanguage,
int? dailyGoal,
}) async {
_setLoading(true);
_clearError();
try {
final response = await _authService.updateProfile(
nickname: nickname,
avatar: avatar,
phone: phone,
birthday: birthday,
gender: gender,
bio: bio,
learningLevel: learningLevel,
targetLanguage: targetLanguage,
nativeLanguage: nativeLanguage,
dailyGoal: dailyGoal,
);
if (response.success && response.data != null) {
_user = response.data;
notifyListeners();
return true;
} else {
_setError(response.message);
return false;
}
} catch (e) {
_setError('更新个人信息失败: $e');
return false;
} finally {
_setLoading(false);
}
}
/// 修改密码
Future<bool> changePassword({
required String currentPassword,
required String newPassword,
required String confirmPassword,
}) async {
_setLoading(true);
_clearError();
try {
final response = await _authService.changePassword(
currentPassword: currentPassword,
newPassword: newPassword,
confirmPassword: confirmPassword,
);
if (response.success) {
return true;
} else {
_setError(response.message);
return false;
}
} catch (e) {
_setError('修改密码失败: $e');
return false;
} finally {
_setLoading(false);
}
}
/// 刷新Token
Future<bool> refreshToken() async {
try {
final response = await _authService.refreshToken();
if (response.success && response.data != null && response.data!.user != null) {
_user = UserModel.fromJson(response.data!.user!);
if (_state != AuthState.authenticated) {
_setState(AuthState.authenticated);
}
return true;
} else {
// Token刷新失败需要重新登录
_user = null;
_setState(AuthState.unauthenticated);
return false;
}
} catch (e) {
_user = null;
_setState(AuthState.unauthenticated);
return false;
}
}
/// 设置加载状态
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
/// 设置状态
void _setState(AuthState state) {
_state = state;
notifyListeners();
}
/// 设置错误信息
void _setError(String message) {
_errorMessage = message;
_setState(AuthState.error);
}
/// 清除错误信息
void _clearError() {
_errorMessage = null;
if (_state == AuthState.error) {
_setState(_user != null ? AuthState.authenticated : AuthState.unauthenticated);
}
}
/// 清除所有状态
void clear() {
_user = null;
_errorMessage = null;
_isLoading = false;
_setState(AuthState.initial);
}
}

View File

@@ -0,0 +1,382 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 错误类型枚举
enum ErrorType {
network,
authentication,
validation,
server,
unknown,
}
/// 错误严重程度枚举
enum ErrorSeverity {
info,
warning,
error,
critical,
}
/// 应用错误模型
class AppError {
final String id;
final String message;
final String? details;
final ErrorType type;
final ErrorSeverity severity;
final DateTime timestamp;
final String? stackTrace;
final Map<String, dynamic>? context;
const AppError({
required this.id,
required this.message,
this.details,
required this.type,
required this.severity,
required this.timestamp,
this.stackTrace,
this.context,
});
AppError copyWith({
String? id,
String? message,
String? details,
ErrorType? type,
ErrorSeverity? severity,
DateTime? timestamp,
String? stackTrace,
Map<String, dynamic>? context,
}) {
return AppError(
id: id ?? this.id,
message: message ?? this.message,
details: details ?? this.details,
type: type ?? this.type,
severity: severity ?? this.severity,
timestamp: timestamp ?? this.timestamp,
stackTrace: stackTrace ?? this.stackTrace,
context: context ?? this.context,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AppError &&
other.id == id &&
other.message == message &&
other.type == type &&
other.severity == severity;
}
@override
int get hashCode {
return id.hashCode ^
message.hashCode ^
type.hashCode ^
severity.hashCode;
}
@override
String toString() {
return 'AppError(id: $id, message: $message, type: $type, severity: $severity, timestamp: $timestamp)';
}
/// 创建网络错误
factory AppError.network(String message, {String? details, Map<String, dynamic>? context}) {
return AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: message,
details: details,
type: ErrorType.network,
severity: ErrorSeverity.error,
timestamp: DateTime.now(),
context: context,
);
}
/// 创建认证错误
factory AppError.authentication(String message, {String? details, Map<String, dynamic>? context}) {
return AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: message,
details: details,
type: ErrorType.authentication,
severity: ErrorSeverity.error,
timestamp: DateTime.now(),
context: context,
);
}
/// 创建验证错误
factory AppError.validation(String message, {String? details, Map<String, dynamic>? context}) {
return AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: message,
details: details,
type: ErrorType.validation,
severity: ErrorSeverity.warning,
timestamp: DateTime.now(),
context: context,
);
}
/// 创建服务器错误
factory AppError.server(String message, {String? details, Map<String, dynamic>? context}) {
return AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: message,
details: details,
type: ErrorType.server,
severity: ErrorSeverity.error,
timestamp: DateTime.now(),
context: context,
);
}
/// 创建未知错误
factory AppError.unknown(String message, {String? details, String? stackTrace, Map<String, dynamic>? context}) {
return AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: message,
details: details,
type: ErrorType.unknown,
severity: ErrorSeverity.error,
timestamp: DateTime.now(),
stackTrace: stackTrace,
context: context,
);
}
}
/// 错误状态模型
class ErrorState {
final List<AppError> errors;
final AppError? currentError;
const ErrorState({
required this.errors,
this.currentError,
});
bool get hasErrors => errors.isNotEmpty;
ErrorState copyWith({
List<AppError>? errors,
AppError? currentError,
}) {
return ErrorState(
errors: errors ?? this.errors,
currentError: currentError,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ErrorState &&
other.errors.length == errors.length &&
other.currentError == currentError;
}
@override
int get hashCode {
return errors.hashCode ^ currentError.hashCode;
}
}
/// 错误管理Provider
class ErrorNotifier extends StateNotifier<ErrorState> {
static const int maxErrorCount = 50; // 最大错误数量
static const Duration errorRetentionDuration = Duration(hours: 24); // 错误保留时间
ErrorNotifier() : super(const ErrorState(errors: []));
/// 添加错误
void addError(AppError error) {
final updatedErrors = List<AppError>.from(state.errors);
updatedErrors.insert(0, error); // 新错误插入到列表开头
// 限制错误数量
if (updatedErrors.length > maxErrorCount) {
updatedErrors.removeRange(maxErrorCount, updatedErrors.length);
}
// 清理过期错误
_cleanExpiredErrors(updatedErrors);
state = state.copyWith(
errors: updatedErrors,
currentError: error,
);
// 在调试模式下打印错误
if (kDebugMode) {
print('Error added: ${error.toString()}');
if (error.stackTrace != null) {
print('Stack trace: ${error.stackTrace}');
}
}
}
/// 清除当前错误
void clearCurrentError() {
state = state.copyWith(currentError: null);
}
/// 清除指定错误
void removeError(String errorId) {
final updatedErrors = state.errors.where((error) => error.id != errorId).toList();
AppError? newCurrentError = state.currentError;
if (state.currentError?.id == errorId) {
newCurrentError = null;
}
state = state.copyWith(
errors: updatedErrors,
currentError: newCurrentError,
);
}
/// 清除所有错误
void clearAllErrors() {
state = const ErrorState(errors: []);
}
/// 清除指定类型的错误
void clearErrorsByType(ErrorType type) {
final updatedErrors = state.errors.where((error) => error.type != type).toList();
AppError? newCurrentError = state.currentError;
if (state.currentError?.type == type) {
newCurrentError = null;
}
state = state.copyWith(
errors: updatedErrors,
currentError: newCurrentError,
);
}
/// 清除指定严重程度的错误
void clearErrorsBySeverity(ErrorSeverity severity) {
final updatedErrors = state.errors.where((error) => error.severity != severity).toList();
AppError? newCurrentError = state.currentError;
if (state.currentError?.severity == severity) {
newCurrentError = null;
}
state = state.copyWith(
errors: updatedErrors,
currentError: newCurrentError,
);
}
/// 获取指定类型的错误
List<AppError> getErrorsByType(ErrorType type) {
return state.errors.where((error) => error.type == type).toList();
}
/// 获取指定严重程度的错误
List<AppError> getErrorsBySeverity(ErrorSeverity severity) {
return state.errors.where((error) => error.severity == severity).toList();
}
/// 获取最近的错误
List<AppError> getRecentErrors({int count = 10}) {
return state.errors.take(count).toList();
}
/// 检查是否有指定类型的错误
bool hasErrorOfType(ErrorType type) {
return state.errors.any((error) => error.type == type);
}
/// 检查是否有指定严重程度的错误
bool hasErrorOfSeverity(ErrorSeverity severity) {
return state.errors.any((error) => error.severity == severity);
}
/// 清理过期错误
void _cleanExpiredErrors(List<AppError> errors) {
final now = DateTime.now();
errors.removeWhere((error) =>
now.difference(error.timestamp) > errorRetentionDuration);
}
/// 处理异常并转换为AppError
void handleException(dynamic exception, {
String? message,
ErrorType? type,
ErrorSeverity? severity,
Map<String, dynamic>? context,
}) {
String errorMessage = message ?? exception.toString();
ErrorType errorType = type ?? ErrorType.unknown;
ErrorSeverity errorSeverity = severity ?? ErrorSeverity.error;
String? stackTrace;
if (exception is Error) {
stackTrace = exception.stackTrace?.toString();
}
final error = AppError(
id: DateTime.now().millisecondsSinceEpoch.toString(),
message: errorMessage,
details: exception.toString(),
type: errorType,
severity: errorSeverity,
timestamp: DateTime.now(),
stackTrace: stackTrace,
context: context,
);
addError(error);
}
}
/// 错误状态Provider
final errorProvider = StateNotifierProvider<ErrorNotifier, ErrorState>(
(ref) => ErrorNotifier(),
);
/// 当前错误Provider
final currentErrorProvider = Provider<AppError?>(
(ref) => ref.watch(errorProvider).currentError,
);
/// 是否有错误Provider
final hasErrorsProvider = Provider<bool>(
(ref) => ref.watch(errorProvider).hasErrors,
);
/// 错误数量Provider
final errorCountProvider = Provider<int>(
(ref) => ref.watch(errorProvider).errors.length,
);
/// 网络错误Provider
final networkErrorsProvider = Provider<List<AppError>>(
(ref) => ref.watch(errorProvider).errors
.where((error) => error.type == ErrorType.network)
.toList(),
);
/// 认证错误Provider
final authErrorsProvider = Provider<List<AppError>>(
(ref) => ref.watch(errorProvider).errors
.where((error) => error.type == ErrorType.authentication)
.toList(),
);
/// 严重错误Provider
final criticalErrorsProvider = Provider<List<AppError>>(
(ref) => ref.watch(errorProvider).errors
.where((error) => error.severity == ErrorSeverity.critical)
.toList(),
);

View File

@@ -0,0 +1,239 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'dart:async';
/// 网络连接状态枚举
enum NetworkStatus {
connected,
disconnected,
unknown,
}
/// 网络连接类型枚举
enum NetworkType {
wifi,
mobile,
ethernet,
none,
unknown,
}
/// 网络状态数据模型
class NetworkState {
final NetworkStatus status;
final NetworkType type;
final bool isOnline;
final DateTime lastChecked;
const NetworkState({
required this.status,
required this.type,
required this.isOnline,
required this.lastChecked,
});
NetworkState copyWith({
NetworkStatus? status,
NetworkType? type,
bool? isOnline,
DateTime? lastChecked,
}) {
return NetworkState(
status: status ?? this.status,
type: type ?? this.type,
isOnline: isOnline ?? this.isOnline,
lastChecked: lastChecked ?? this.lastChecked,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is NetworkState &&
other.status == status &&
other.type == type &&
other.isOnline == isOnline;
}
@override
int get hashCode {
return status.hashCode ^
type.hashCode ^
isOnline.hashCode;
}
@override
String toString() {
return 'NetworkState(status: $status, type: $type, isOnline: $isOnline, lastChecked: $lastChecked)';
}
}
/// 网络状态管理Provider
class NetworkNotifier extends StateNotifier<NetworkState> {
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
final Connectivity _connectivity = Connectivity();
NetworkNotifier()
: super(NetworkState(
status: NetworkStatus.unknown,
type: NetworkType.unknown,
isOnline: false,
lastChecked: DateTime.now(),
)) {
_initializeNetworkMonitoring();
}
/// 初始化网络监控
void _initializeNetworkMonitoring() {
// 监听网络连接状态变化
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) {
_updateNetworkState(results);
},
);
// 初始检查网络状态
_checkInitialConnectivity();
}
/// 检查初始网络连接状态
Future<void> _checkInitialConnectivity() async {
try {
final results = await _connectivity.checkConnectivity();
_updateNetworkState(results);
} catch (e) {
if (kDebugMode) {
print('Error checking initial connectivity: $e');
}
state = state.copyWith(
status: NetworkStatus.unknown,
type: NetworkType.unknown,
isOnline: false,
lastChecked: DateTime.now(),
);
}
}
/// 更新网络状态
void _updateNetworkState(List<ConnectivityResult> results) {
// 取第一个有效的连接结果,如果列表为空则认为无连接
final result = results.isNotEmpty ? results.first : ConnectivityResult.none;
final networkType = _mapConnectivityResultToNetworkType(result);
final isOnline = result != ConnectivityResult.none;
final status = isOnline ? NetworkStatus.connected : NetworkStatus.disconnected;
state = NetworkState(
status: status,
type: networkType,
isOnline: isOnline,
lastChecked: DateTime.now(),
);
if (kDebugMode) {
print('Network state updated: $state');
}
}
/// 将ConnectivityResult映射到NetworkType
NetworkType _mapConnectivityResultToNetworkType(ConnectivityResult result) {
switch (result) {
case ConnectivityResult.wifi:
return NetworkType.wifi;
case ConnectivityResult.mobile:
return NetworkType.mobile;
case ConnectivityResult.ethernet:
return NetworkType.ethernet;
case ConnectivityResult.none:
return NetworkType.none;
default:
return NetworkType.unknown;
}
}
/// 手动刷新网络状态
Future<void> refreshNetworkStatus() async {
try {
final results = await _connectivity.checkConnectivity();
_updateNetworkState(results);
} catch (e) {
if (kDebugMode) {
print('Error refreshing network status: $e');
}
}
}
/// 检查是否有网络连接
bool get isConnected => state.isOnline;
/// 检查是否为WiFi连接
bool get isWifiConnected => state.type == NetworkType.wifi && state.isOnline;
/// 检查是否为移动网络连接
bool get isMobileConnected => state.type == NetworkType.mobile && state.isOnline;
/// 获取网络类型描述
String get networkTypeDescription {
switch (state.type) {
case NetworkType.wifi:
return 'WiFi';
case NetworkType.mobile:
return '移动网络';
case NetworkType.ethernet:
return '以太网';
case NetworkType.none:
return '无网络';
case NetworkType.unknown:
return '未知网络';
}
}
/// 获取网络状态描述
String get statusDescription {
switch (state.status) {
case NetworkStatus.connected:
return '已连接';
case NetworkStatus.disconnected:
return '已断开';
case NetworkStatus.unknown:
return '未知状态';
}
}
@override
void dispose() {
_connectivitySubscription.cancel();
super.dispose();
}
}
/// 网络状态Provider
final networkProvider = StateNotifierProvider<NetworkNotifier, NetworkState>(
(ref) => NetworkNotifier(),
);
/// 网络连接状态Provider简化版
final isConnectedProvider = Provider<bool>(
(ref) => ref.watch(networkProvider).isOnline,
);
/// 网络类型Provider
final networkTypeProvider = Provider<NetworkType>(
(ref) => ref.watch(networkProvider).type,
);
/// WiFi连接状态Provider
final isWifiConnectedProvider = Provider<bool>(
(ref) {
final networkState = ref.watch(networkProvider);
return networkState.type == NetworkType.wifi && networkState.isOnline;
},
);
/// 移动网络连接状态Provider
final isMobileConnectedProvider = Provider<bool>(
(ref) {
final networkState = ref.watch(networkProvider);
return networkState.type == NetworkType.mobile && networkState.isOnline;
},
);

View File

@@ -0,0 +1,222 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/notification_model.dart';
import '../services/notification_service.dart';
/// 通知状态
class NotificationState {
final List<NotificationModel> notifications;
final int total;
final int unreadCount;
final bool isLoading;
final String? error;
NotificationState({
this.notifications = const [],
this.total = 0,
this.unreadCount = 0,
this.isLoading = false,
this.error,
});
NotificationState copyWith({
List<NotificationModel>? notifications,
int? total,
int? unreadCount,
bool? isLoading,
String? error,
}) {
return NotificationState(
notifications: notifications ?? this.notifications,
total: total ?? this.total,
unreadCount: unreadCount ?? this.unreadCount,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
/// 通知Notifier
class NotificationNotifier extends StateNotifier<NotificationState> {
final NotificationService _service = NotificationService();
NotificationNotifier() : super(NotificationState());
/// 加载通知列表
Future<void> loadNotifications({
int page = 1,
int limit = 10,
bool onlyUnread = false,
}) async {
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _service.getNotifications(
page: page,
limit: limit,
onlyUnread: onlyUnread,
);
if (response.success && response.data != null) {
state = state.copyWith(
notifications: response.data!['notifications'] as List<NotificationModel>,
total: response.data!['total'] as int,
isLoading: false,
);
} else {
state = state.copyWith(
isLoading: false,
error: response.message,
);
}
} catch (e) {
state = state.copyWith(
isLoading: false,
error: '加载通知列表失败: $e',
);
}
}
/// 加载未读通知数量
Future<void> loadUnreadCount() async {
try {
final response = await _service.getUnreadCount();
if (response.success && response.data != null) {
state = state.copyWith(
unreadCount: response.data!,
);
}
} catch (e) {
print('加载未读通知数量失败: $e');
}
}
/// 标记通知为已读
Future<bool> markAsRead(int notificationId) async {
try {
final response = await _service.markAsRead(notificationId);
if (response.success) {
// 更新本地状态
final updatedNotifications = state.notifications.map((n) {
if (n.id == notificationId) {
return NotificationModel(
id: n.id,
userId: n.userId,
type: n.type,
title: n.title,
content: n.content,
link: n.link,
isRead: true,
priority: n.priority,
createdAt: n.createdAt,
readAt: DateTime.now(),
);
}
return n;
}).toList();
state = state.copyWith(
notifications: updatedNotifications,
unreadCount: state.unreadCount > 0 ? state.unreadCount - 1 : 0,
);
return true;
}
return false;
} catch (e) {
print('标记通知已读失败: $e');
return false;
}
}
/// 标记所有通知为已读
Future<bool> markAllAsRead() async {
try {
final response = await _service.markAllAsRead();
if (response.success) {
// 更新本地状态
final updatedNotifications = state.notifications.map((n) {
return NotificationModel(
id: n.id,
userId: n.userId,
type: n.type,
title: n.title,
content: n.content,
link: n.link,
isRead: true,
priority: n.priority,
createdAt: n.createdAt,
readAt: DateTime.now(),
);
}).toList();
state = state.copyWith(
notifications: updatedNotifications,
unreadCount: 0,
);
return true;
}
return false;
} catch (e) {
print('标记所有通知已读失败: $e');
return false;
}
}
/// 删除通知
Future<bool> deleteNotification(int notificationId) async {
try {
final response = await _service.deleteNotification(notificationId);
if (response.success) {
// 从本地状态中移除
final updatedNotifications =
state.notifications.where((n) => n.id != notificationId).toList();
// 如果删除的是未读通知,更新未读数量
final deletedNotification =
state.notifications.firstWhere((n) => n.id == notificationId);
final newUnreadCount = deletedNotification.isRead
? state.unreadCount
: (state.unreadCount > 0 ? state.unreadCount - 1 : 0);
state = state.copyWith(
notifications: updatedNotifications,
total: state.total - 1,
unreadCount: newUnreadCount,
);
return true;
}
return false;
} catch (e) {
print('删除通知失败: $e');
return false;
}
}
/// 刷新通知列表
Future<void> refresh() async {
await loadNotifications();
await loadUnreadCount();
}
}
/// 通知Provider
final notificationProvider =
StateNotifierProvider<NotificationNotifier, NotificationState>(
(ref) => NotificationNotifier(),
);
/// 未读通知数量Provider便捷访问
final unreadCountProvider = Provider<int>((ref) {
return ref.watch(notificationProvider).unreadCount;
});
/// 通知列表Provider便捷访问
final notificationListProvider = Provider<List<NotificationModel>>((ref) {
return ref.watch(notificationProvider).notifications;
});

View File

@@ -0,0 +1,369 @@
import 'package:flutter/foundation.dart';
import '../models/vocabulary_model.dart';
import '../models/api_response.dart';
import '../services/vocabulary_service.dart';
import '../../core/errors/app_error.dart';
/// 词汇学习状态
enum VocabularyState {
initial,
loading,
loaded,
error,
}
/// 词汇学习Provider
class VocabularyProvider with ChangeNotifier {
final VocabularyService _vocabularyService;
VocabularyProvider(this._vocabularyService);
// 状态管理
VocabularyState _state = VocabularyState.initial;
String? _errorMessage;
// 词库数据
List<VocabularyBookModel> _vocabularyBooks = [];
VocabularyBookModel? _currentBook;
// 词汇数据
List<VocabularyModel> _vocabularies = [];
List<UserVocabularyModel> _userVocabularies = [];
VocabularyModel? _currentVocabulary;
// 学习数据
List<VocabularyModel> _todayReviewWords = [];
List<VocabularyModel> _newWords = [];
Map<String, dynamic> _learningStats = {};
// 分页数据
int _currentPage = 1;
bool _hasMoreData = true;
// Getters
VocabularyState get state => _state;
String? get errorMessage => _errorMessage;
List<VocabularyBookModel> get vocabularyBooks => _vocabularyBooks;
VocabularyBookModel? get currentBook => _currentBook;
List<VocabularyModel> get vocabularies => _vocabularies;
List<UserVocabularyModel> get userVocabularies => _userVocabularies;
VocabularyModel? get currentVocabulary => _currentVocabulary;
List<VocabularyModel> get todayReviewWords => _todayReviewWords;
List<VocabularyModel> get newWords => _newWords;
Map<String, dynamic> get learningStats => _learningStats;
int get currentPage => _currentPage;
bool get hasMoreData => _hasMoreData;
/// 设置状态
void _setState(VocabularyState newState, [String? error]) {
_state = newState;
_errorMessage = error;
notifyListeners();
}
/// 获取词库列表
Future<void> loadVocabularyBooks() async {
try {
_setState(VocabularyState.loading);
final response = await _vocabularyService.getVocabularyBooks();
if (response.success && response.data != null) {
_vocabularyBooks = response.data!;
_setState(VocabularyState.loaded);
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 设置当前词库
void setCurrentBook(VocabularyBookModel book) {
_currentBook = book;
_vocabularies.clear();
_currentPage = 1;
_hasMoreData = true;
notifyListeners();
}
/// 获取词汇列表
Future<void> loadVocabularies({
String? bookId,
String? search,
String? difficulty,
bool loadMore = false,
}) async {
try {
if (!loadMore) {
_setState(VocabularyState.loading);
_currentPage = 1;
_vocabularies.clear();
}
final response = await _vocabularyService.getVocabularies(
bookId: bookId ?? _currentBook?.bookId.toString(),
page: _currentPage,
search: search,
level: difficulty,
);
if (response.success && response.data != null) {
final newVocabularies = response.data!.data;
if (loadMore) {
_vocabularies.addAll(newVocabularies);
} else {
_vocabularies = newVocabularies;
}
_hasMoreData = response.data!.pagination.hasNextPage;
_currentPage++;
_setState(VocabularyState.loaded);
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 获取单词详情
Future<VocabularyModel?> getWordDetail(String wordId) async {
try {
final response = await _vocabularyService.getWordDetail(wordId);
if (response.success && response.data != null) {
_currentVocabulary = response.data!;
notifyListeners();
return response.data!;
} else {
_setState(VocabularyState.error, response.message);
return null;
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
return null;
}
}
/// 获取用户词汇学习记录
Future<void> loadUserVocabularies(String userId) async {
try {
final response = await _vocabularyService.getUserVocabularies();
if (response.success && response.data != null) {
_userVocabularies = response.data!.data;
notifyListeners();
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 更新单词学习状态
Future<bool> updateWordStatus(String wordId, LearningStatus status) async {
try {
final response = await _vocabularyService.updateWordStatus(
wordId: wordId,
status: status,
);
if (response.success) {
// 更新本地数据
final index = _userVocabularies.indexWhere((uv) => uv.wordId.toString() == wordId);
if (index != -1) {
// TODO: 实现UserVocabularyModel的copyWith方法
// _userVocabularies[index] = _userVocabularies[index].copyWith(
// status: status,
// lastStudiedAt: DateTime.now(),
// );
notifyListeners();
}
return true;
} else {
_setState(VocabularyState.error, response.message);
return false;
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
return false;
}
}
/// 添加单词到学习列表
Future<bool> addToLearningList(String wordId) async {
try {
// TODO: 实现addToLearningList方法
// final response = await _vocabularyService.addToLearningList(wordId);
final response = ApiResponse.success(message: 'Added to learning list');
if (response.success) {
// 刷新用户词汇数据
await loadUserVocabularies('current_user_id'); // TODO: 获取当前用户ID
return true;
} else {
_setState(VocabularyState.error, response.message);
return false;
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
return false;
}
}
/// 从学习列表移除单词
Future<bool> removeFromLearningList(String wordId) async {
try {
// TODO: 实现removeFromLearningList方法
// final response = await _vocabularyService.removeFromLearningList(wordId);
final response = ApiResponse.success(message: 'Removed from learning list');
if (response.success) {
// 更新本地数据
_userVocabularies.removeWhere((uv) => uv.wordId.toString() == wordId);
notifyListeners();
return true;
} else {
_setState(VocabularyState.error, response.message);
return false;
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
return false;
}
}
/// 获取今日复习单词
Future<void> loadTodayReviewWords() async {
try {
// TODO: 实现getTodayReviewWords方法
// final response = await _vocabularyService.getTodayReviewWords();
final response = ApiResponse<List<VocabularyModel>>.success(
message: 'Today review words retrieved',
data: <VocabularyModel>[],
);
if (response.success && response.data != null) {
_todayReviewWords = response.data ?? [];
notifyListeners();
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 获取新单词学习
Future<void> loadNewWords({int limit = 20}) async {
try {
// TODO: 实现getNewWordsForLearning方法
// final response = await _vocabularyService.getNewWordsForLearning(limit: limit);
final response = ApiResponse<List<VocabularyModel>>.success(
message: 'New words retrieved',
data: <VocabularyModel>[],
);
if (response.success && response.data != null) {
_newWords = response.data ?? [];
notifyListeners();
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 进行词汇量测试
Future<Map<String, dynamic>?> takeVocabularyTest(List<Map<String, dynamic>> answers) async {
try {
_setState(VocabularyState.loading);
// TODO: 实现takeVocabularyTest方法
// final response = await _vocabularyService.takeVocabularyTest(answers);
final response = ApiResponse<Map<String, dynamic>>.success(
message: 'Vocabulary test completed',
data: <String, dynamic>{},
);
if (response.success && response.data != null) {
_setState(VocabularyState.loaded);
return response.data!;
} else {
_setState(VocabularyState.error, response.message);
return null;
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
return null;
}
}
/// 获取学习统计
Future<void> loadLearningStats() async {
try {
// TODO: 实现getLearningStats方法
// final response = await _vocabularyService.getLearningStats();
final response = ApiResponse<Map<String, dynamic>>.success(
message: 'Learning stats retrieved',
data: <String, dynamic>{},
);
if (response.success && response.data != null) {
_learningStats = response.data ?? {};
notifyListeners();
} else {
_setState(VocabularyState.error, response.message);
}
} catch (e) {
_setState(VocabularyState.error, e.toString());
}
}
/// 搜索词汇
Future<void> searchVocabularies(String query) async {
await loadVocabularies(search: query);
}
/// 按难度筛选词汇
Future<void> filterByDifficulty(String difficulty) async {
await loadVocabularies(difficulty: difficulty);
}
/// 加载更多词汇
Future<void> loadMoreVocabularies() async {
if (_hasMoreData && _state != VocabularyState.loading) {
await loadVocabularies(loadMore: true);
}
}
/// 清除错误状态
void clearError() {
_errorMessage = null;
if (_state == VocabularyState.error) {
_setState(VocabularyState.initial);
}
}
/// 重置状态
void reset() {
_state = VocabularyState.initial;
_errorMessage = null;
_vocabularyBooks.clear();
_currentBook = null;
_vocabularies.clear();
_userVocabularies.clear();
_currentVocabulary = null;
_todayReviewWords.clear();
_newWords.clear();
_learningStats.clear();
_currentPage = 1;
_hasMoreData = true;
notifyListeners();
}
}

View File

@@ -0,0 +1,332 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:dio/dio.dart';
import '../../core/network/api_client.dart';
import '../../core/errors/app_error.dart';
import '../models/api_response.dart';
/// 音频播放状态
enum AudioPlayerState {
stopped,
playing,
paused,
loading,
error,
}
/// 音频服务
class AudioService {
static final AudioService _instance = AudioService._internal();
factory AudioService() => _instance;
AudioService._internal();
final ApiClient _apiClient = ApiClient.instance;
AudioPlayerState _state = AudioPlayerState.stopped;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
double _volume = 1.0;
double _playbackRate = 1.0;
String? _currentUrl;
// 回调函数
Function(AudioPlayerState)? onStateChanged;
Function(Duration)? onDurationChanged;
Function(Duration)? onPositionChanged;
Function(String)? onError;
/// 更新播放状态
void _updateState(AudioPlayerState state) {
_state = state;
onStateChanged?.call(state);
}
/// 播放网络音频
Future<void> playFromUrl(String url) async {
try {
_updateState(AudioPlayerState.loading);
_currentUrl = url;
// TODO: 实现音频播放逻辑
_updateState(AudioPlayerState.playing);
} catch (e) {
_updateState(AudioPlayerState.error);
onError?.call('播放失败: $e');
}
}
/// 播放本地音频文件
Future<void> playFromFile(String filePath) async {
try {
_updateState(AudioPlayerState.loading);
_currentUrl = filePath;
// TODO: 实现本地音频播放逻辑
_updateState(AudioPlayerState.playing);
} catch (e) {
_updateState(AudioPlayerState.error);
onError?.call('播放失败: $e');
}
}
/// 播放资源文件
Future<void> playFromAsset(String assetPath) async {
try {
_updateState(AudioPlayerState.loading);
_currentUrl = assetPath;
// TODO: 实现资源音频播放逻辑
_updateState(AudioPlayerState.playing);
} catch (e) {
_updateState(AudioPlayerState.error);
onError?.call('播放失败: $e');
}
}
/// 暂停播放
Future<void> pause() async {
try {
// TODO: 实现暂停逻辑
_updateState(AudioPlayerState.paused);
} catch (e) {
onError?.call('暂停失败: $e');
}
}
/// 恢复播放
Future<void> resume() async {
try {
// TODO: 实现恢复播放逻辑
_updateState(AudioPlayerState.playing);
} catch (e) {
onError?.call('恢复播放失败: $e');
}
}
/// 停止播放
Future<void> stop() async {
try {
// TODO: 实现停止播放逻辑
_updateState(AudioPlayerState.stopped);
_position = Duration.zero;
} catch (e) {
onError?.call('停止播放失败: $e');
}
}
/// 跳转到指定位置
Future<void> seek(Duration position) async {
try {
// TODO: 实现跳转逻辑
_position = position;
} catch (e) {
onError?.call('跳转失败: $e');
}
}
/// 设置音量 (0.0 - 1.0)
Future<void> setVolume(double volume) async {
try {
_volume = volume.clamp(0.0, 1.0);
// TODO: 实现音量设置逻辑
} catch (e) {
onError?.call('设置音量失败: $e');
}
}
/// 设置播放速度 (0.5 - 2.0)
Future<void> setPlaybackRate(double rate) async {
try {
_playbackRate = rate.clamp(0.5, 2.0);
// TODO: 实现播放速度设置逻辑
} catch (e) {
onError?.call('设置播放速度失败: $e');
}
}
/// 下载音频文件
Future<ApiResponse<String>> downloadAudio({
required String url,
required String fileName,
Function(int, int)? onProgress,
}) async {
try {
final directory = await getApplicationDocumentsDirectory();
final audioDir = Directory('${directory.path}/audio');
if (!await audioDir.exists()) {
await audioDir.create(recursive: true);
}
final filePath = '${audioDir.path}/$fileName';
// TODO: 实现文件下载逻辑
final response = await _apiClient.get(url);
final file = File(filePath);
await file.writeAsBytes(response.data);
return ApiResponse.success(
message: '音频下载成功',
data: filePath,
);
} catch (e) {
return ApiResponse.failure(
message: '音频下载失败: $e',
error: e.toString(),
);
}
}
/// 上传音频文件
Future<ApiResponse<Map<String, dynamic>>> uploadAudio({
required String filePath,
required String type, // 'pronunciation', 'speaking', etc.
Map<String, dynamic>? metadata,
Function(int, int)? onProgress,
}) async {
try {
final file = File(filePath);
if (!await file.exists()) {
return ApiResponse.failure(
message: '音频文件不存在',
error: 'FILE_NOT_FOUND',
);
}
final formData = FormData.fromMap({
'audio': await MultipartFile.fromFile(
filePath,
filename: file.path.split('/').last,
),
'type': type,
if (metadata != null) 'metadata': metadata,
});
final response = await _apiClient.post(
'/audio/upload',
data: formData,
);
if (response.statusCode == 200) {
return ApiResponse.success(
message: response.data['message'] ?? '音频上传成功',
data: response.data['data'],
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? '音频上传失败',
code: response.statusCode,
);
}
} catch (e) {
return ApiResponse.failure(
message: '音频上传失败: $e',
error: e.toString(),
);
}
}
/// 获取音频文件信息
Future<ApiResponse<Map<String, dynamic>>> getAudioInfo(String audioId) async {
try {
final response = await _apiClient.get('/audio/$audioId');
if (response.statusCode == 200) {
return ApiResponse.success(
message: 'Audio info retrieved successfully',
data: response.data['data'],
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get audio info',
code: response.statusCode,
);
}
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get audio info: $e',
error: e.toString(),
);
}
}
/// 删除本地音频文件
Future<bool> deleteLocalAudio(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
return true;
}
return false;
} catch (e) {
onError?.call('删除音频文件失败: $e');
return false;
}
}
/// 清理缓存的音频文件
Future<void> clearAudioCache() async {
try {
final directory = await getApplicationDocumentsDirectory();
final audioDir = Directory('${directory.path}/audio');
if (await audioDir.exists()) {
await audioDir.delete(recursive: true);
}
} catch (e) {
onError?.call('清理音频缓存失败: $e');
}
}
/// 检查音频文件是否存在
Future<bool> isAudioCached(String fileName) async {
try {
final directory = await getApplicationDocumentsDirectory();
final filePath = '${directory.path}/audio/$fileName';
final file = File(filePath);
return await file.exists();
} catch (e) {
return false;
}
}
/// 获取缓存音频文件路径
Future<String?> getCachedAudioPath(String fileName) async {
try {
final directory = await getApplicationDocumentsDirectory();
final filePath = '${directory.path}/audio/$fileName';
final file = File(filePath);
if (await file.exists()) {
return filePath;
}
return null;
} catch (e) {
return null;
}
}
/// 释放资源
Future<void> dispose() async {
// TODO: 实现资源释放逻辑
}
// Getters
AudioPlayerState get state => _state;
Duration get duration => _duration;
Duration get position => _position;
double get volume => _volume;
double get playbackRate => _playbackRate;
String? get currentUrl => _currentUrl;
bool get isPlaying => _state == AudioPlayerState.playing;
bool get isPaused => _state == AudioPlayerState.paused;
bool get isStopped => _state == AudioPlayerState.stopped;
bool get isLoading => _state == AudioPlayerState.loading;
/// 获取播放进度百分比 (0.0 - 1.0)
double get progress {
if (_duration.inMilliseconds == 0) return 0.0;
return _position.inMilliseconds / _duration.inMilliseconds;
}
}

View File

@@ -0,0 +1,371 @@
import 'package:dio/dio.dart';
import '../../core/network/api_client.dart';
import '../../core/storage/storage_service.dart';
import '../../core/constants/app_constants.dart';
import '../../core/errors/app_error.dart';
import '../models/api_response.dart';
import '../models/user_model.dart';
/// 认证服务
class AuthService {
static final AuthService _instance = AuthService._internal();
factory AuthService() => _instance;
AuthService._internal();
final ApiClient _apiClient = ApiClient.instance;
/// 用户注册
Future<ApiResponse<AuthResponse>> register({
required String username,
required String email,
required String password,
required String nickname,
String? phone,
}) async {
try {
final response = await _apiClient.post(
'/auth/register',
data: {
'username': username,
'email': email,
'password': password,
'nickname': nickname,
if (phone != null) 'phone': phone,
},
);
if (response.statusCode == 201) {
final authResponse = AuthResponse.fromJson(response.data['data']);
await _saveTokens(authResponse);
return ApiResponse.success(
message: response.data['message'] ?? '注册成功',
data: authResponse,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? '注册失败',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: '注册失败:$e',
error: e.toString(),
);
}
}
/// 用户登录
Future<ApiResponse<AuthResponse>> login({
required String account, // 用户名或邮箱
required String password,
bool rememberMe = false,
}) async {
try {
final response = await _apiClient.post(
'/auth/login',
data: {
'account': account,
'password': password,
},
);
if (response.statusCode == 200) {
final authResponse = AuthResponse.fromJson(response.data['data']);
await _saveTokens(authResponse);
return ApiResponse.success(
message: response.data['message'] ?? '登录成功',
data: authResponse,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? '登录失败',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: '登录失败:$e',
error: e.toString(),
);
}
}
/// 刷新Token
Future<ApiResponse<AuthResponse>> refreshToken() async {
try {
final refreshToken = StorageService.getString(AppConstants.refreshTokenKey);
if (refreshToken == null) {
return ApiResponse.failure(
message: 'Refresh token not found',
code: 401,
);
}
final response = await _apiClient.post(
'/auth/refresh',
data: {
'refresh_token': refreshToken,
},
);
if (response.statusCode == 200) {
final authResponse = AuthResponse.fromJson(response.data['data']);
await _saveTokens(authResponse);
return ApiResponse.success(
message: 'Token refreshed successfully',
data: authResponse,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Token refresh failed',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Token refresh failed: $e',
error: e.toString(),
);
}
}
/// 用户登出
Future<ApiResponse<void>> logout() async {
try {
final response = await _apiClient.post('/auth/logout');
// 无论服务器响应如何都清除本地token
await _clearTokens();
if (response.statusCode == 200) {
return ApiResponse.success(
message: response.data['message'] ?? '登出成功',
);
} else {
return ApiResponse.success(
message: '登出成功',
);
}
} on DioException catch (e) {
// 即使请求失败也清除本地token
await _clearTokens();
return ApiResponse.success(
message: '登出成功',
);
} catch (e) {
await _clearTokens();
return ApiResponse.success(
message: '登出成功',
);
}
}
/// 获取当前用户信息
Future<ApiResponse<UserModel>> getCurrentUser() async {
try {
final response = await _apiClient.get('/auth/me');
if (response.statusCode == 200) {
final user = UserModel.fromJson(response.data['data']);
await StorageService.setObject(AppConstants.userInfoKey, user.toJson());
return ApiResponse.success(
message: 'User info retrieved successfully',
data: user,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get user info',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get user info: $e',
error: e.toString(),
);
}
}
/// 更新用户信息
Future<ApiResponse<UserModel>> updateProfile({
String? nickname,
String? avatar,
String? phone,
DateTime? birthday,
String? gender,
String? bio,
String? learningLevel,
String? targetLanguage,
String? nativeLanguage,
int? dailyGoal,
}) async {
try {
final data = <String, dynamic>{};
if (nickname != null) data['nickname'] = nickname;
if (avatar != null) data['avatar'] = avatar;
if (phone != null) data['phone'] = phone;
if (birthday != null) data['birthday'] = birthday.toIso8601String();
if (gender != null) data['gender'] = gender;
if (bio != null) data['bio'] = bio;
if (learningLevel != null) data['learning_level'] = learningLevel;
if (targetLanguage != null) data['target_language'] = targetLanguage;
if (nativeLanguage != null) data['native_language'] = nativeLanguage;
if (dailyGoal != null) data['daily_goal'] = dailyGoal;
final response = await _apiClient.put('/auth/profile', data: data);
if (response.statusCode == 200) {
final user = UserModel.fromJson(response.data['data']);
await StorageService.setObject(AppConstants.userInfoKey, user.toJson());
return ApiResponse.success(
message: response.data['message'] ?? '个人信息更新成功',
data: user,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? '个人信息更新失败',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: '个人信息更新失败:$e',
error: e.toString(),
);
}
}
/// 修改密码
Future<ApiResponse<void>> changePassword({
required String currentPassword,
required String newPassword,
required String confirmPassword,
}) async {
try {
final response = await _apiClient.put(
'/auth/change-password',
data: {
'current_password': currentPassword,
'new_password': newPassword,
'confirm_password': confirmPassword,
},
);
if (response.statusCode == 200) {
return ApiResponse.success(
message: response.data['message'] ?? '密码修改成功',
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? '密码修改失败',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: '密码修改失败:$e',
error: e.toString(),
);
}
}
/// 检查是否已登录
bool isLoggedIn() {
final token = StorageService.getString(AppConstants.accessTokenKey);
return token != null && token.isNotEmpty;
}
/// 获取本地存储的用户信息
UserModel? getCachedUser() {
final userJson = StorageService.getObject(AppConstants.userInfoKey);
if (userJson != null) {
try {
return UserModel.fromJson(userJson);
} catch (e) {
print('Error parsing cached user: $e');
return null;
}
}
return null;
}
/// 保存tokens
Future<void> _saveTokens(AuthResponse authResponse) async {
await StorageService.setString(
AppConstants.accessTokenKey,
authResponse.accessToken,
);
await StorageService.setString(
AppConstants.refreshTokenKey,
authResponse.refreshToken,
);
if (authResponse.user != null) {
await StorageService.setObject(
AppConstants.userInfoKey,
authResponse.user!,
);
}
}
/// 清除tokens
Future<void> _clearTokens() async {
await StorageService.remove(AppConstants.accessTokenKey);
await StorageService.remove(AppConstants.refreshTokenKey);
await StorageService.remove(AppConstants.userInfoKey);
}
/// 处理Dio错误
ApiResponse<T> _handleDioError<T>(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiResponse.failure(
message: '请求超时,请检查网络连接',
error: 'TIMEOUT',
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final message = e.response?.data?['message'] ?? '请求失败';
return ApiResponse.failure(
message: message,
code: statusCode,
error: 'BAD_RESPONSE',
);
case DioExceptionType.cancel:
return ApiResponse.failure(
message: '请求已取消',
error: 'CANCELLED',
);
case DioExceptionType.connectionError:
return ApiResponse.failure(
message: '网络连接失败,请检查网络设置',
error: 'CONNECTION_ERROR',
);
default:
return ApiResponse.failure(
message: '未知错误:${e.message}',
error: 'UNKNOWN',
);
}
}
}

View File

@@ -0,0 +1,109 @@
import '../../../core/models/api_response.dart';
import '../../../core/services/enhanced_api_service.dart';
import '../models/notification_model.dart';
/// 通知服务
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final EnhancedApiService _enhancedApiService = EnhancedApiService();
// 缓存时长配置
static const Duration _shortCacheDuration = Duration(seconds: 30);
/// 获取通知列表
Future<ApiResponse<Map<String, dynamic>>> getNotifications({
int page = 1,
int limit = 10,
bool onlyUnread = false,
}) async {
try {
final response = await _enhancedApiService.get<Map<String, dynamic>>(
'/notifications',
queryParameters: {
'page': page,
'limit': limit,
'only_unread': onlyUnread,
},
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final notifications = (data['notifications'] as List?)
?.map((json) => NotificationModel.fromJson(json))
.toList() ??
[];
return {
'notifications': notifications,
'total': data['total'] ?? 0,
'page': data['page'] ?? 1,
'limit': data['limit'] ?? 10,
};
},
);
return response;
} catch (e) {
print('获取通知列表异常: $e');
return ApiResponse.error(message: '获取通知列表失败: $e');
}
}
/// 获取未读通知数量
Future<ApiResponse<int>> getUnreadCount() async {
try {
final response = await _enhancedApiService.get<int>(
'/notifications/unread-count',
cacheDuration: _shortCacheDuration,
fromJson: (data) => data['count'] ?? 0,
);
return response;
} catch (e) {
print('获取未读通知数量异常: $e');
return ApiResponse.error(message: '获取未读通知数量失败: $e');
}
}
/// 标记通知为已读
Future<ApiResponse<void>> markAsRead(int notificationId) async {
try {
final response = await _enhancedApiService.put<void>(
'/notifications/$notificationId/read',
);
return response;
} catch (e) {
print('标记通知已读异常: $e');
return ApiResponse.error(message: '标记通知已读失败: $e');
}
}
/// 标记所有通知为已读
Future<ApiResponse<void>> markAllAsRead() async {
try {
final response = await _enhancedApiService.put<void>(
'/notifications/read-all',
);
return response;
} catch (e) {
print('标记所有通知已读异常: $e');
return ApiResponse.error(message: '标记所有通知已读失败: $e');
}
}
/// 删除通知
Future<ApiResponse<void>> deleteNotification(int notificationId) async {
try {
final response = await _enhancedApiService.delete<void>(
'/notifications/$notificationId',
);
return response;
} catch (e) {
print('删除通知异常: $e');
return ApiResponse.error(message: '删除通知失败: $e');
}
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../shared/services/api_service.dart';
/// 学习计划服务
class StudyPlanService {
final ApiService _apiService;
StudyPlanService(this._apiService);
/// 创建学习计划
Future<Map<String, dynamic>> createStudyPlan({
required String planName,
String? description,
required int dailyGoal,
String? bookId,
required String startDate,
String? endDate,
String? remindTime,
String? remindDays,
}) async {
final response = await _apiService.post(
'/study-plans',
data: {
'plan_name': planName,
'description': description,
'daily_goal': dailyGoal,
'book_id': bookId,
'start_date': startDate,
'end_date': endDate,
'remind_time': remindTime,
'remind_days': remindDays,
},
);
return response['data'];
}
/// 获取学习计划列表
Future<List<dynamic>> getUserStudyPlans({String status = 'all'}) async {
final response = await _apiService.get(
'/study-plans',
queryParameters: {'status': status},
);
return response['data']['plans'];
}
/// 获取今日学习计划
Future<List<dynamic>> getTodayStudyPlans() async {
final response = await _apiService.get('/study-plans/today');
return response['data']['plans'];
}
/// 获取学习计划详情
Future<Map<String, dynamic>> getStudyPlanByID(int planId) async {
final response = await _apiService.get('/study-plans/$planId');
return response['data'];
}
/// 更新学习计划
Future<Map<String, dynamic>> updateStudyPlan(
int planId,
Map<String, dynamic> updates,
) async {
final response = await _apiService.put('/study-plans/$planId', data: updates);
return response['data'];
}
/// 删除学习计划
Future<void> deleteStudyPlan(int planId) async {
await _apiService.delete('/study-plans/$planId');
}
/// 更新计划状态
Future<void> updatePlanStatus(int planId, String status) async {
await _apiService.patch(
'/study-plans/$planId/status',
data: {'status': status},
);
}
/// 记录学习进度
Future<void> recordStudyProgress(
int planId, {
required int wordsStudied,
int studyDuration = 0,
}) async {
await _apiService.post(
'/study-plans/$planId/progress',
data: {
'words_studied': wordsStudied,
'study_duration': studyDuration,
},
);
}
/// 获取计划统计
Future<Map<String, dynamic>> getStudyPlanStatistics(int planId) async {
final response = await _apiService.get('/study-plans/$planId/statistics');
return response['data'];
}
}
/// 学习计划服务提供者
final studyPlanServiceProvider = Provider<StudyPlanService>((ref) {
final apiService = ref.watch(apiServiceProvider);
return StudyPlanService(apiService);
});

View File

@@ -0,0 +1,479 @@
import 'package:dio/dio.dart';
import '../../core/network/api_client.dart';
import '../../core/constants/app_constants.dart';
import '../../core/errors/app_error.dart';
import '../models/api_response.dart';
import '../models/vocabulary_model.dart';
/// 词汇服务
class VocabularyService {
static final VocabularyService _instance = VocabularyService._internal();
factory VocabularyService() => _instance;
VocabularyService._internal();
final ApiClient _apiClient = ApiClient.instance;
/// 获取词库列表
Future<ApiResponse<List<VocabularyBookModel>>> getVocabularyBooks({
String? category,
String? level,
int page = 1,
int limit = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
if (category != null) queryParams['category'] = category;
if (level != null) queryParams['level'] = level;
final response = await _apiClient.get(
'/vocabulary/books',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> booksJson = response.data['data']['items'];
final books = booksJson
.map((json) => VocabularyBookModel.fromJson(json))
.toList();
return ApiResponse.success(
message: 'Vocabulary books retrieved successfully',
data: books,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get vocabulary books',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get vocabulary books: $e',
error: e.toString(),
);
}
}
/// 获取词汇列表
Future<ApiResponse<PaginatedResponse<VocabularyModel>>> getVocabularies({
String? bookId,
String? search,
String? level,
int page = 1,
int limit = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
if (bookId != null) queryParams['book_id'] = bookId;
if (search != null) queryParams['search'] = search;
if (level != null) queryParams['level'] = level;
final response = await _apiClient.get(
'/vocabulary/words',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final paginatedResponse = PaginatedResponse<VocabularyModel>.fromJson(
response.data['data'],
(json) => VocabularyModel.fromJson(json),
);
return ApiResponse.success(
message: 'Vocabularies retrieved successfully',
data: paginatedResponse,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get vocabularies',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get vocabularies: $e',
error: e.toString(),
);
}
}
/// 获取单词详情
Future<ApiResponse<VocabularyModel>> getWordDetail(String wordId) async {
try {
final response = await _apiClient.get('/vocabulary/words/$wordId');
if (response.statusCode == 200) {
final word = VocabularyModel.fromJson(response.data['data']);
return ApiResponse.success(
message: 'Word detail retrieved successfully',
data: word,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get word detail',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get word detail: $e',
error: e.toString(),
);
}
}
/// 获取用户词汇学习记录
Future<ApiResponse<PaginatedResponse<UserVocabularyModel>>> getUserVocabularies({
LearningStatus? status,
String? bookId,
int page = 1,
int limit = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
if (status != null) queryParams['status'] = status.name;
if (bookId != null) queryParams['book_id'] = bookId;
final response = await _apiClient.get(
'/vocabulary/user-words',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final paginatedResponse = PaginatedResponse<UserVocabularyModel>.fromJson(
response.data['data'],
(json) => UserVocabularyModel.fromJson(json),
);
return ApiResponse.success(
message: 'User vocabularies retrieved successfully',
data: paginatedResponse,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get user vocabularies',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get user vocabularies: $e',
error: e.toString(),
);
}
}
/// 更新单词学习状态
Future<ApiResponse<UserVocabularyModel>> updateWordStatus({
required String wordId,
required LearningStatus status,
int? correctCount,
int? wrongCount,
int? reviewCount,
DateTime? nextReviewDate,
Map<String, dynamic>? metadata,
}) async {
try {
final data = <String, dynamic>{
'status': status.name,
};
if (correctCount != null) data['correct_count'] = correctCount;
if (wrongCount != null) data['wrong_count'] = wrongCount;
if (reviewCount != null) data['review_count'] = reviewCount;
if (nextReviewDate != null) {
data['next_review_date'] = nextReviewDate.toIso8601String();
}
if (metadata != null) data['metadata'] = metadata;
final response = await _apiClient.put(
'/vocabulary/user-words/$wordId',
data: data,
);
if (response.statusCode == 200) {
final userWord = UserVocabularyModel.fromJson(response.data['data']);
return ApiResponse.success(
message: response.data['message'] ?? 'Word status updated successfully',
data: userWord,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to update word status',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to update word status: $e',
error: e.toString(),
);
}
}
/// 添加单词到学习列表
Future<ApiResponse<UserVocabularyModel>> addWordToLearning(String wordId) async {
try {
final response = await _apiClient.post(
'/vocabulary/user-words',
data: {'word_id': wordId},
);
if (response.statusCode == 201) {
final userWord = UserVocabularyModel.fromJson(response.data['data']);
return ApiResponse.success(
message: response.data['message'] ?? 'Word added to learning list',
data: userWord,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to add word to learning list',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to add word to learning list: $e',
error: e.toString(),
);
}
}
/// 从学习列表移除单词
Future<ApiResponse<void>> removeWordFromLearning(String wordId) async {
try {
final response = await _apiClient.delete('/vocabulary/user-words/$wordId');
if (response.statusCode == 200) {
return ApiResponse.success(
message: response.data['message'] ?? 'Word removed from learning list',
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to remove word from learning list',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to remove word from learning list: $e',
error: e.toString(),
);
}
}
/// 获取今日复习单词
Future<ApiResponse<List<UserVocabularyModel>>> getTodayReviewWords() async {
try {
final response = await _apiClient.get('/vocabulary/today-review');
if (response.statusCode == 200) {
final List<dynamic> wordsJson = response.data['data'];
final words = wordsJson
.map((json) => UserVocabularyModel.fromJson(json))
.toList();
return ApiResponse.success(
message: 'Today review words retrieved successfully',
data: words,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get today review words',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get today review words: $e',
error: e.toString(),
);
}
}
/// 获取新单词学习
Future<ApiResponse<List<VocabularyModel>>> getNewWordsForLearning({
String? bookId,
int limit = 10,
}) async {
try {
final queryParams = <String, dynamic>{
'limit': limit,
};
if (bookId != null) queryParams['book_id'] = bookId;
final response = await _apiClient.get(
'/vocabulary/new-words',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> wordsJson = response.data['data'];
final words = wordsJson
.map((json) => VocabularyModel.fromJson(json))
.toList();
return ApiResponse.success(
message: 'New words for learning retrieved successfully',
data: words,
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get new words for learning',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get new words for learning: $e',
error: e.toString(),
);
}
}
/// 词汇量测试
Future<ApiResponse<Map<String, dynamic>>> vocabularyTest({
required List<String> wordIds,
required List<String> answers,
}) async {
try {
final response = await _apiClient.post(
'/vocabulary/test',
data: {
'word_ids': wordIds,
'answers': answers,
},
);
if (response.statusCode == 200) {
return ApiResponse.success(
message: 'Vocabulary test completed successfully',
data: response.data['data'],
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Vocabulary test failed',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Vocabulary test failed: $e',
error: e.toString(),
);
}
}
/// 获取学习统计
Future<ApiResponse<Map<String, dynamic>>> getLearningStats({
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final queryParams = <String, dynamic>{};
if (startDate != null) {
queryParams['start_date'] = startDate.toIso8601String();
}
if (endDate != null) {
queryParams['end_date'] = endDate.toIso8601String();
}
final response = await _apiClient.get(
'/vocabulary/stats',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
return ApiResponse.success(
message: 'Learning stats retrieved successfully',
data: response.data['data'],
);
} else {
return ApiResponse.failure(
message: response.data['message'] ?? 'Failed to get learning stats',
code: response.statusCode,
);
}
} on DioException catch (e) {
return _handleDioError(e);
} catch (e) {
return ApiResponse.failure(
message: 'Failed to get learning stats: $e',
error: e.toString(),
);
}
}
/// 处理Dio错误
ApiResponse<T> _handleDioError<T>(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiResponse.failure(
message: '请求超时,请检查网络连接',
error: 'TIMEOUT',
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final message = e.response?.data?['message'] ?? '请求失败';
return ApiResponse.failure(
message: message,
code: statusCode,
error: 'BAD_RESPONSE',
);
case DioExceptionType.cancel:
return ApiResponse.failure(
message: '请求已取消',
error: 'CANCELLED',
);
case DioExceptionType.connectionError:
return ApiResponse.failure(
message: '网络连接失败,请检查网络设置',
error: 'CONNECTION_ERROR',
);
default:
return ApiResponse.failure(
message: '未知错误:${e.message}',
error: 'UNKNOWN',
);
}
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/network/api_client.dart';
/// 生词本服务
class WordBookService {
final ApiClient _apiClient = ApiClient.instance;
WordBookService();
/// 切换单词收藏状态
Future<Map<String, dynamic>> toggleFavorite(int wordId) async {
final response = await _apiClient.post('/word-book/toggle/$wordId');
return response.data['data'];
}
/// 获取生词本列表
Future<Map<String, dynamic>> getFavoriteWords({
int page = 1,
int pageSize = 20,
String sortBy = 'created_at',
String order = 'desc',
}) async {
final response = await _apiClient.get(
'/word-book',
queryParameters: {
'page': page,
'page_size': pageSize,
'sort_by': sortBy,
'order': order,
},
);
return response.data['data'];
}
/// 获取指定词汇书的生词本
Future<List<dynamic>> getFavoriteWordsByBook(String bookId) async {
final response = await _apiClient.get('/word-book/books/$bookId');
return response.data['data']['words'];
}
/// 获取生词本统计信息
Future<Map<String, dynamic>> getFavoriteStats() async {
final response = await _apiClient.get('/word-book/stats');
return response.data['data'];
}
/// 批量添加到生词本
Future<int> batchAddToFavorite(List<int> wordIds) async {
final response = await _apiClient.post(
'/word-book/batch',
data: {'word_ids': wordIds},
);
return response.data['data']['added_count'];
}
/// 从生词本移除单词
Future<void> removeFromFavorite(int wordId) async {
await _apiClient.delete('/word-book/$wordId');
}
}
/// 生词本服务提供者
final wordBookServiceProvider = Provider<WordBookService>((ref) {
return WordBookService();
});

View File

@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
/// 自定义应用栏
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool centerTitle;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
final bool automaticallyImplyLeading;
final PreferredSizeWidget? bottom;
final VoidCallback? onBackPressed;
const CustomAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.centerTitle = true,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
this.automaticallyImplyLeading = true,
this.bottom,
this.onBackPressed,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppBar(
title: Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: foregroundColor ?? theme.colorScheme.onSurface,
),
),
centerTitle: centerTitle,
backgroundColor: backgroundColor ?? theme.colorScheme.surface,
foregroundColor: foregroundColor ?? theme.colorScheme.onSurface,
elevation: elevation,
automaticallyImplyLeading: automaticallyImplyLeading,
leading: leading ?? (onBackPressed != null
? IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: onBackPressed,
)
: null),
actions: actions,
bottom: bottom,
surfaceTintColor: Colors.transparent,
);
}
@override
Size get preferredSize => Size.fromHeight(
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
);
}
/// 带搜索功能的应用栏
class SearchAppBar extends StatefulWidget implements PreferredSizeWidget {
final String title;
final String hintText;
final ValueChanged<String>? onSearchChanged;
final VoidCallback? onSearchSubmitted;
final List<Widget>? actions;
final bool automaticallyImplyLeading;
final VoidCallback? onBackPressed;
const SearchAppBar({
super.key,
required this.title,
this.hintText = '搜索...',
this.onSearchChanged,
this.onSearchSubmitted,
this.actions,
this.automaticallyImplyLeading = true,
this.onBackPressed,
});
@override
State<SearchAppBar> createState() => _SearchAppBarState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _SearchAppBarState extends State<SearchAppBar> {
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
void _startSearch() {
setState(() {
_isSearching = true;
});
_searchFocusNode.requestFocus();
}
void _stopSearch() {
setState(() {
_isSearching = false;
_searchController.clear();
});
_searchFocusNode.unfocus();
widget.onSearchChanged?.call('');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppBar(
title: _isSearching
? TextField(
controller: _searchController,
focusNode: _searchFocusNode,
decoration: InputDecoration(
hintText: widget.hintText,
border: InputBorder.none,
hintStyle: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
style: theme.textTheme.bodyLarge,
onChanged: widget.onSearchChanged,
onSubmitted: (_) => widget.onSearchSubmitted?.call(),
)
: Text(
widget.title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
centerTitle: !_isSearching,
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
elevation: 0,
automaticallyImplyLeading: widget.automaticallyImplyLeading && !_isSearching,
leading: _isSearching
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _stopSearch,
)
: (widget.onBackPressed != null
? IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: widget.onBackPressed,
)
: null),
actions: _isSearching
? [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
widget.onSearchChanged?.call('');
},
),
]
: [
IconButton(
icon: const Icon(Icons.search),
onPressed: _startSearch,
),
...?widget.actions,
],
surfaceTintColor: Colors.transparent,
);
}
}
/// 带标签页的应用栏
class TabAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Tab> tabs;
final TabController? controller;
final List<Widget>? actions;
final bool automaticallyImplyLeading;
final VoidCallback? onBackPressed;
const TabAppBar({
super.key,
required this.title,
required this.tabs,
this.controller,
this.actions,
this.automaticallyImplyLeading = true,
this.onBackPressed,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppBar(
title: Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
elevation: 0,
automaticallyImplyLeading: automaticallyImplyLeading,
leading: onBackPressed != null
? IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: onBackPressed,
)
: null,
actions: actions,
bottom: TabBar(
controller: controller,
tabs: tabs,
labelColor: theme.colorScheme.primary,
unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.6),
indicatorColor: theme.colorScheme.primary,
indicatorWeight: 2,
labelStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: theme.textTheme.titleSmall,
),
surfaceTintColor: Colors.transparent,
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + kTextTabBarHeight);
}

View File

@@ -0,0 +1,440 @@
import 'package:flutter/material.dart';
/// 自定义卡片组件
class CustomCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final Color? color;
final double? elevation;
final BorderRadius? borderRadius;
final Border? border;
final VoidCallback? onTap;
final bool isSelected;
final Color? selectedColor;
final double? width;
final double? height;
final BoxShadow? shadow;
const CustomCard({
super.key,
required this.child,
this.padding,
this.margin,
this.color,
this.elevation,
this.borderRadius,
this.border,
this.onTap,
this.isSelected = false,
this.selectedColor,
this.width,
this.height,
this.shadow,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final defaultBorderRadius = BorderRadius.circular(12);
Widget card = Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
color: isSelected
? (selectedColor ?? theme.colorScheme.primaryContainer)
: (color ?? theme.colorScheme.surface),
borderRadius: borderRadius ?? defaultBorderRadius,
border: border ?? (isSelected
? Border.all(
color: theme.colorScheme.primary,
width: 2,
)
: Border.all(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
)),
boxShadow: shadow != null
? [shadow!]
: [
BoxShadow(
color: theme.colorScheme.shadow.withOpacity(0.1),
blurRadius: elevation ?? 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: borderRadius ?? defaultBorderRadius,
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
),
),
);
return card;
}
}
/// 信息卡片
class InfoCard extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final Color? backgroundColor;
final bool isSelected;
const InfoCard({
super.key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.onTap,
this.padding,
this.margin,
this.backgroundColor,
this.isSelected = false,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CustomCard(
onTap: onTap,
padding: padding ?? const EdgeInsets.all(16),
margin: margin,
color: backgroundColor,
isSelected: isSelected,
child: Row(
children: [
if (leading != null) ...[
leading!,
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected
? theme.colorScheme.onPrimaryContainer
: theme.colorScheme.onSurface,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: theme.textTheme.bodyMedium?.copyWith(
color: isSelected
? theme.colorScheme.onPrimaryContainer.withOpacity(0.8)
: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
],
],
),
),
if (trailing != null) ...[
const SizedBox(width: 12),
trailing!,
],
],
),
);
}
}
/// 统计卡片
class StatCard extends StatelessWidget {
final String title;
final String value;
final String? unit;
final IconData? icon;
final Color? iconColor;
final Color? backgroundColor;
final VoidCallback? onTap;
final EdgeInsetsGeometry? margin;
final Widget? trailing;
const StatCard({
super.key,
required this.title,
required this.value,
this.unit,
this.icon,
this.iconColor,
this.backgroundColor,
this.onTap,
this.margin,
this.trailing,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CustomCard(
onTap: onTap,
margin: margin,
color: backgroundColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
if (icon != null) ...[
Icon(
icon,
size: 20,
color: iconColor ?? theme.colorScheme.primary,
),
const SizedBox(width: 8),
],
Expanded(
child: Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
),
if (trailing != null) trailing!,
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
if (unit != null) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
unit!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
),
],
],
),
],
),
);
}
}
/// 功能卡片
class FeatureCard extends StatelessWidget {
final String title;
final String? description;
final IconData icon;
final Color? iconColor;
final Color? backgroundColor;
final VoidCallback? onTap;
final EdgeInsetsGeometry? margin;
final bool isEnabled;
final Widget? badge;
const FeatureCard({
super.key,
required this.title,
this.description,
required this.icon,
this.iconColor,
this.backgroundColor,
this.onTap,
this.margin,
this.isEnabled = true,
this.badge,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CustomCard(
onTap: isEnabled ? onTap : null,
margin: margin,
color: backgroundColor,
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: (iconColor ?? theme.colorScheme.primary).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 24,
color: isEnabled
? (iconColor ?? theme.colorScheme.primary)
: theme.colorScheme.onSurface.withOpacity(0.4),
),
),
const SizedBox(height: 12),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isEnabled
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withOpacity(0.4),
),
),
if (description != null) ...[
const SizedBox(height: 4),
Text(
description!,
style: theme.textTheme.bodySmall?.copyWith(
color: isEnabled
? theme.colorScheme.onSurface.withOpacity(0.7)
: theme.colorScheme.onSurface.withOpacity(0.4),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
if (badge != null)
Positioned(
top: 0,
right: 0,
child: badge!,
),
],
),
);
}
}
/// 进度卡片
class ProgressCard extends StatelessWidget {
final String title;
final String? subtitle;
final double progress;
final String? progressText;
final Color? progressColor;
final Color? backgroundColor;
final VoidCallback? onTap;
final EdgeInsetsGeometry? margin;
final Widget? trailing;
const ProgressCard({
super.key,
required this.title,
this.subtitle,
required this.progress,
this.progressText,
this.progressColor,
this.backgroundColor,
this.onTap,
this.margin,
this.trailing,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CustomCard(
onTap: onTap,
margin: margin,
color: backgroundColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
],
],
),
),
if (trailing != null) ...[
const SizedBox(width: 12),
trailing!,
],
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: theme.colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
progressColor ?? theme.colorScheme.primary,
),
minHeight: 6,
),
),
if (progressText != null) ...[
const SizedBox(width: 12),
Text(
progressText!,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface.withOpacity(0.8),
),
),
],
],
),
],
),
);
}
}

View File

@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/error_provider.dart';
/// 全局错误处理组件
class GlobalErrorHandler extends ConsumerWidget {
final Widget child;
const GlobalErrorHandler({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
final errorState = ref.watch(errorProvider);
// 监听错误状态变化
ref.listen<ErrorState>(errorProvider, (previous, next) {
if (next.currentError != null) {
_showErrorDialog(context, next.currentError!, ref);
}
});
return child;
}
void _showErrorDialog(BuildContext context, AppError error, WidgetRef ref) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => ErrorDialog(
error: error,
onDismiss: () {
Navigator.of(context).pop();
ref.read(errorProvider.notifier).removeError(error.id);
},
onRetry: null,
),
);
}
}
/// 错误对话框
class ErrorDialog extends StatelessWidget {
final AppError error;
final VoidCallback onDismiss;
final VoidCallback? onRetry;
const ErrorDialog({
super.key,
required this.error,
required this.onDismiss,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(
_getErrorIcon(),
color: _getErrorColor(),
),
const SizedBox(width: 8),
Text(_getErrorTitle()),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(error.message),
if (error.details != null) ...[
const SizedBox(height: 8),
Text(
error.details!,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
actions: [
if (onRetry != null)
TextButton(
onPressed: () {
onDismiss();
onRetry!();
},
child: const Text('重试'),
),
TextButton(
onPressed: onDismiss,
child: const Text('确定'),
),
],
);
}
IconData _getErrorIcon() {
switch (error.type) {
case ErrorType.network:
return Icons.wifi_off;
case ErrorType.authentication:
return Icons.lock;
case ErrorType.validation:
return Icons.warning;
case ErrorType.server:
return Icons.error;
case ErrorType.unknown:
default:
return Icons.help_outline;
}
}
Color _getErrorColor() {
switch (error.severity) {
case ErrorSeverity.critical:
return Colors.red;
case ErrorSeverity.error:
return Colors.orange;
case ErrorSeverity.warning:
return Colors.yellow[700]!;
case ErrorSeverity.info:
return Colors.blue;
}
}
String _getErrorTitle() {
switch (error.type) {
case ErrorType.network:
return '网络错误';
case ErrorType.authentication:
return '认证错误';
case ErrorType.validation:
return '验证错误';
case ErrorType.server:
return '服务器错误';
case ErrorType.unknown:
default:
return '未知错误';
}
}
}
/// 错误横幅组件
class ErrorBanner extends ConsumerWidget {
const ErrorBanner({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final errorState = ref.watch(errorProvider);
if (!errorState.hasErrors) {
return const SizedBox.shrink();
}
final lowSeverityErrors = errorState.errors
.where((error) => error.severity == ErrorSeverity.info)
.toList();
if (lowSeverityErrors.isEmpty) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
color: Colors.blue[50],
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.blue[700],
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
lowSeverityErrors.first.message,
style: TextStyle(
color: Colors.blue[700],
fontSize: 14,
),
),
),
IconButton(
onPressed: () {
ref.read(errorProvider.notifier).removeError(lowSeverityErrors.first.id);
},
icon: Icon(
Icons.close,
color: Colors.blue[700],
size: 20,
),
),
],
),
);
}
}
/// 错误重试组件
class ErrorRetryWidget extends StatelessWidget {
final String message;
final VoidCallback onRetry;
final IconData? icon;
const ErrorRetryWidget({
super.key,
required this.message,
required this.onRetry,
this.icon,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon ?? Icons.error_outline,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
message,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
),
);
}
}

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
/// 错误显示组件
class ErrorDisplayWidget extends StatelessWidget {
final String message;
final String? title;
final IconData? icon;
final VoidCallback? onRetry;
final String? retryText;
final EdgeInsetsGeometry? padding;
final bool showIcon;
final Color? iconColor;
final TextAlign textAlign;
const ErrorDisplayWidget({
super.key,
required this.message,
this.title,
this.icon,
this.onRetry,
this.retryText,
this.padding,
this.showIcon = true,
this.iconColor,
this.textAlign = TextAlign.center,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: padding ?? const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (showIcon) ...[
Icon(
icon ?? Icons.error_outline,
size: 64,
color: iconColor ?? theme.colorScheme.error,
),
const SizedBox(height: 16),
],
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
textAlign: textAlign,
),
const SizedBox(height: 8),
],
Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: textAlign,
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: Text(retryText ?? '重试'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
],
),
);
}
}
/// 页面错误组件
class PageErrorWidget extends StatelessWidget {
final String message;
final String? title;
final VoidCallback? onRetry;
final bool showAppBar;
final String? appBarTitle;
final VoidCallback? onBack;
const PageErrorWidget({
super.key,
required this.message,
this.title,
this.onRetry,
this.showAppBar = false,
this.appBarTitle,
this.onBack,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: showAppBar
? AppBar(
title: appBarTitle != null ? Text(appBarTitle!) : null,
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
leading: onBack != null
? IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: onBack,
)
: null,
)
: null,
body: Center(
child: ErrorDisplayWidget(
title: title ?? '出错了',
message: message,
onRetry: onRetry,
),
),
);
}
}
/// 网络错误组件
class NetworkErrorWidget extends StatelessWidget {
final VoidCallback? onRetry;
final String? customMessage;
final EdgeInsetsGeometry? padding;
const NetworkErrorWidget({
super.key,
this.onRetry,
this.customMessage,
this.padding,
});
@override
Widget build(BuildContext context) {
return ErrorDisplayWidget(
title: '网络连接失败',
message: customMessage ?? '请检查网络连接后重试',
icon: Icons.wifi_off,
onRetry: onRetry,
padding: padding,
);
}
}
/// 空数据组件
class EmptyDataWidget extends StatelessWidget {
final String message;
final String? title;
final IconData? icon;
final VoidCallback? onAction;
final String? actionText;
final EdgeInsetsGeometry? padding;
final Color? iconColor;
const EmptyDataWidget({
super.key,
required this.message,
this.title,
this.icon,
this.onAction,
this.actionText,
this.padding,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: padding ?? const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.inbox_outlined,
size: 64,
color: iconColor ?? theme.colorScheme.onSurface.withOpacity(0.4),
),
const SizedBox(height: 16),
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
if (onAction != null) ...[
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: onAction,
icon: const Icon(Icons.add),
label: Text(actionText ?? '添加'),
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.primary,
side: BorderSide(color: theme.colorScheme.primary),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
],
),
);
}
}
/// 搜索无结果组件
class NoSearchResultWidget extends StatelessWidget {
final String query;
final VoidCallback? onClear;
final EdgeInsetsGeometry? padding;
const NoSearchResultWidget({
super.key,
required this.query,
this.onClear,
this.padding,
});
@override
Widget build(BuildContext context) {
return EmptyDataWidget(
title: '未找到相关结果',
message: '没有找到与"$query"相关的内容\n请尝试其他关键词',
icon: Icons.search_off,
onAction: onClear,
actionText: '清除搜索',
padding: padding,
);
}
}
/// 权限错误组件
class PermissionErrorWidget extends StatelessWidget {
final String message;
final VoidCallback? onRequestPermission;
final EdgeInsetsGeometry? padding;
const PermissionErrorWidget({
super.key,
required this.message,
this.onRequestPermission,
this.padding,
});
@override
Widget build(BuildContext context) {
return ErrorDisplayWidget(
title: '权限不足',
message: message,
icon: Icons.lock_outline,
onRetry: onRequestPermission,
retryText: '授权',
padding: padding,
iconColor: Theme.of(context).colorScheme.warning,
);
}
}
/// 服务器错误组件
class ServerErrorWidget extends StatelessWidget {
final String? customMessage;
final VoidCallback? onRetry;
final EdgeInsetsGeometry? padding;
const ServerErrorWidget({
super.key,
this.customMessage,
this.onRetry,
this.padding,
});
@override
Widget build(BuildContext context) {
return ErrorDisplayWidget(
title: '服务器错误',
message: customMessage ?? '服务器暂时无法响应,请稍后重试',
icon: Icons.cloud_off,
onRetry: onRetry,
padding: padding,
);
}
}
/// 通用错误处理器
class ErrorHandler {
static Widget handleError(
Object error, {
VoidCallback? onRetry,
EdgeInsetsGeometry? padding,
}) {
if (error.toString().contains('network') ||
error.toString().contains('connection')) {
return NetworkErrorWidget(
onRetry: onRetry,
padding: padding,
);
}
if (error.toString().contains('permission')) {
return PermissionErrorWidget(
message: error.toString(),
onRequestPermission: onRetry,
padding: padding,
);
}
if (error.toString().contains('server') ||
error.toString().contains('500')) {
return ServerErrorWidget(
onRetry: onRetry,
padding: padding,
);
}
return ErrorDisplayWidget(
message: error.toString(),
onRetry: onRetry,
padding: padding,
);
}
}
/// 错误边界组件
class ErrorBoundary extends StatefulWidget {
final Widget child;
final Widget Function(Object error)? errorBuilder;
final void Function(Object error, StackTrace stackTrace)? onError;
const ErrorBoundary({
super.key,
required this.child,
this.errorBuilder,
this.onError,
});
@override
State<ErrorBoundary> createState() => _ErrorBoundaryState();
}
class _ErrorBoundaryState extends State<ErrorBoundary> {
Object? _error;
@override
Widget build(BuildContext context) {
if (_error != null) {
return widget.errorBuilder?.call(_error!) ??
ErrorHandler.handleError(_error!);
}
return widget.child;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 重置错误状态
if (_error != null) {
setState(() {
_error = null;
});
}
}
void _handleError(Object error, StackTrace stackTrace) {
widget.onError?.call(error, stackTrace);
if (mounted) {
setState(() {
_error = error;
});
}
}
}
/// 扩展 ColorScheme 以支持警告颜色
extension ColorSchemeExtension on ColorScheme {
Color get warning => const Color(0xFFFF9800);
Color get onWarning => const Color(0xFF000000);
}

View File

@@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
/// 加载组件
class LoadingWidget extends StatelessWidget {
final String? message;
final double? size;
final Color? color;
final EdgeInsetsGeometry? padding;
final bool showMessage;
const LoadingWidget({
super.key,
this.message,
this.size,
this.color,
this.padding,
this.showMessage = true,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: padding ?? const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size ?? 32,
height: size ?? 32,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
color ?? theme.colorScheme.primary,
),
),
),
if (showMessage && message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}
/// 页面加载组件
class PageLoadingWidget extends StatelessWidget {
final String? message;
final bool showAppBar;
final String? title;
const PageLoadingWidget({
super.key,
this.message,
this.showAppBar = false,
this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: showAppBar
? AppBar(
title: title != null ? Text(title!) : null,
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
)
: null,
body: Center(
child: LoadingWidget(
message: message ?? '加载中...',
size: 48,
),
),
);
}
}
/// 列表加载组件
class ListLoadingWidget extends StatelessWidget {
final int itemCount;
final double itemHeight;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
const ListLoadingWidget({
super.key,
this.itemCount = 5,
this.itemHeight = 80,
this.padding,
this.margin,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: padding,
itemCount: itemCount,
itemBuilder: (context, index) => Container(
height: itemHeight,
margin: margin ?? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: const ShimmerWidget(),
),
);
}
}
/// 骨架屏组件
class ShimmerWidget extends StatefulWidget {
final double? width;
final double? height;
final BorderRadius? borderRadius;
final Color? baseColor;
final Color? highlightColor;
const ShimmerWidget({
super.key,
this.width,
this.height,
this.borderRadius,
this.baseColor,
this.highlightColor,
});
@override
State<ShimmerWidget> createState() => _ShimmerWidgetState();
}
class _ShimmerWidgetState extends State<ShimmerWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = Tween<double>(
begin: -1.0,
end: 2.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final baseColor = widget.baseColor ?? theme.colorScheme.surfaceVariant;
final highlightColor = widget.highlightColor ??
theme.colorScheme.surface.withOpacity(0.8);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
baseColor,
highlightColor,
baseColor,
],
stops: [
_animation.value - 0.3,
_animation.value,
_animation.value + 0.3,
],
),
),
);
},
);
}
}
/// 卡片骨架屏
class CardShimmerWidget extends StatelessWidget {
final EdgeInsetsGeometry? margin;
final double? height;
const CardShimmerWidget({
super.key,
this.margin,
this.height,
});
@override
Widget build(BuildContext context) {
return Container(
margin: margin ?? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const ShimmerWidget(
width: 40,
height: 40,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerWidget(
width: double.infinity,
height: 16,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: 120,
height: 12,
borderRadius: BorderRadius.circular(4),
),
],
),
),
],
),
if (height != null && height! > 100) ...[
const SizedBox(height: 16),
ShimmerWidget(
width: double.infinity,
height: 12,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: double.infinity,
height: 12,
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
ShimmerWidget(
width: 200,
height: 12,
borderRadius: BorderRadius.circular(4),
),
],
],
),
);
}
}
/// 按钮加载状态
class LoadingButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final Color? backgroundColor;
final Color? foregroundColor;
final EdgeInsetsGeometry? padding;
final double? width;
final double? height;
final BorderRadius? borderRadius;
const LoadingButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
this.backgroundColor,
this.foregroundColor,
this.padding,
this.width,
this.height,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: width,
height: height ?? 48,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? theme.colorScheme.primary,
foregroundColor: foregroundColor ?? theme.colorScheme.onPrimary,
padding: padding ?? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(8),
),
elevation: 0,
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
foregroundColor ?? theme.colorScheme.onPrimary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(
text,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
/// 刷新指示器
class RefreshIndicatorWidget extends StatelessWidget {
final Widget child;
final Future<void> Function() onRefresh;
final String? refreshText;
const RefreshIndicatorWidget({
super.key,
required this.child,
required this.onRefresh,
this.refreshText,
});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
color: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surface,
child: child,
);
}
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/network_provider.dart';
/// 网络状态指示器
class NetworkIndicator extends ConsumerWidget {
final Widget child;
final bool showBanner;
const NetworkIndicator({
super.key,
required this.child,
this.showBanner = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final networkState = ref.watch(networkProvider);
return Column(
children: [
if (showBanner && networkState.status == NetworkStatus.disconnected)
_buildOfflineBanner(context),
Expanded(child: child),
],
);
}
Widget _buildOfflineBanner(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.red[600],
child: Row(
children: [
const Icon(
Icons.wifi_off,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
const Expanded(
child: Text(
'网络连接已断开',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
TextButton(
onPressed: () {
// 可以添加重试逻辑
},
child: const Text(
'重试',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
),
],
),
);
}
}
/// 网络状态图标
class NetworkStatusIcon extends ConsumerWidget {
final double size;
final Color? color;
const NetworkStatusIcon({
super.key,
this.size = 24,
this.color,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final networkState = ref.watch(networkProvider);
return Icon(
_getNetworkIcon(networkState.status, networkState.type),
size: size,
color: color ?? _getNetworkColor(networkState.status),
);
}
IconData _getNetworkIcon(NetworkStatus status, NetworkType type) {
if (status == NetworkStatus.disconnected) {
return Icons.wifi_off;
}
switch (type) {
case NetworkType.wifi:
return Icons.wifi;
case NetworkType.mobile:
return Icons.signal_cellular_4_bar;
case NetworkType.ethernet:
return Icons.cable;
case NetworkType.unknown:
default:
return Icons.device_unknown;
}
}
Color _getNetworkColor(NetworkStatus status) {
switch (status) {
case NetworkStatus.connected:
return Colors.green;
case NetworkStatus.disconnected:
return Colors.red;
case NetworkStatus.unknown:
return Colors.grey;
}
}
}
/// 网络状态卡片
class NetworkStatusCard extends ConsumerWidget {
const NetworkStatusCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final networkState = ref.watch(networkProvider);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
NetworkStatusIcon(
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getStatusText(networkState.status),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
_getTypeText(networkState.type),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'最后更新: ${_formatTime(networkState.lastChecked)}',
style: Theme.of(context).textTheme.bodySmall,
),
TextButton(
onPressed: () {
ref.read(networkProvider.notifier).refreshNetworkStatus();
},
child: const Text('刷新'),
),
],
),
],
),
),
);
}
String _getStatusText(NetworkStatus status) {
switch (status) {
case NetworkStatus.connected:
return '已连接';
case NetworkStatus.disconnected:
return '未连接';
case NetworkStatus.unknown:
return '未知状态';
}
}
String _getTypeText(NetworkType type) {
switch (type) {
case NetworkType.wifi:
return 'Wi-Fi';
case NetworkType.mobile:
return '移动网络';
case NetworkType.ethernet:
return '以太网';
case NetworkType.unknown:
default:
return '未知';
}
}
String _formatTime(DateTime time) {
final now = DateTime.now();
final difference = now.difference(time);
if (difference.inMinutes < 1) {
return '刚刚';
} else if (difference.inHours < 1) {
return '${difference.inMinutes}分钟前';
} else if (difference.inDays < 1) {
return '${difference.inHours}小时前';
} else {
return '${difference.inDays}天前';
}
}
}