Initial commit: Flutter 无书应用项目
This commit is contained in:
613
lib/views/profile/level/level-jilu.dart
Normal file
613
lib/views/profile/level/level-jilu.dart
Normal file
@@ -0,0 +1,613 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../utils/http/http_client.dart';
|
||||
import '../../../services/network_listener_service.dart';
|
||||
|
||||
/// 时间: 2026-03-28
|
||||
/// 功能: 诗词答题逻辑管理器
|
||||
/// 介绍: 处理诗词答题的网络请求和业务逻辑
|
||||
/// 最新变化: 添加离线模式支持,离线时从本地缓存加载题目
|
||||
|
||||
class PoetryLevelManager with NetworkListenerMixin {
|
||||
int _currentIndex = 0;
|
||||
int _total = 0;
|
||||
List<int> _shuffledIds = [];
|
||||
final Random _random = Random();
|
||||
|
||||
// 离线模式相关
|
||||
bool _isOfflineMode = false;
|
||||
List<Map<String, dynamic>> _offlineQuestions = [];
|
||||
bool _isOnline = true;
|
||||
|
||||
/// 获取当前题目 ID
|
||||
int get currentId =>
|
||||
_shuffledIds.isNotEmpty ? _shuffledIds[_currentIndex] : 0;
|
||||
|
||||
/// 获取当前题目索引
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
/// 获取题目总数
|
||||
int get total => _total;
|
||||
|
||||
/// 是否已初始化
|
||||
bool get isInitialized =>
|
||||
_shuffledIds.isNotEmpty || _offlineQuestions.isNotEmpty;
|
||||
|
||||
/// 是否为离线模式
|
||||
bool get isOfflineMode => _isOfflineMode;
|
||||
|
||||
/// 检查网络状态
|
||||
Future<bool> _checkNetworkStatus() async {
|
||||
try {
|
||||
final networkStatus = NetworkListenerService().currentStatus;
|
||||
return networkStatus != NetworkStatus.error;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否为离线状态(用户设置)
|
||||
Future<bool> _checkOfflineSetting() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return !(prefs.getBool('personal_card_online') ?? true);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否有缓存数据
|
||||
Future<bool> _hasCachedData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final quizData = prefs.getStringList('offline_quiz_data') ?? [];
|
||||
return quizData.isNotEmpty;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载离线缓存数据
|
||||
Future<void> _loadOfflineCache() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final quizData = prefs.getStringList('offline_quiz_data') ?? [];
|
||||
|
||||
_offlineQuestions = [];
|
||||
for (final item in quizData) {
|
||||
try {
|
||||
final map = _parseStringToMap(item);
|
||||
if (map.isNotEmpty) {
|
||||
_offlineQuestions.add(map);
|
||||
}
|
||||
} catch (e) {
|
||||
print('解析离线数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (_offlineQuestions.isNotEmpty) {
|
||||
_total = _offlineQuestions.length;
|
||||
_shuffledIds = List.generate(_total, (index) => index);
|
||||
_shuffleList(_shuffledIds);
|
||||
_currentIndex = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
print('加载离线缓存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析字符串为Map
|
||||
Map<String, dynamic> _parseStringToMap(String str) {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
print('开始解析字符串: $str');
|
||||
|
||||
try {
|
||||
// 尝试JSON解析
|
||||
final jsonMap = jsonDecode(str);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
print('JSON解析成功: $jsonMap');
|
||||
return jsonMap;
|
||||
}
|
||||
} catch (e) {
|
||||
print('JSON解析失败: $e');
|
||||
// 不是JSON格式,尝试其他解析方式
|
||||
}
|
||||
|
||||
// 尝试解析 Map 字符串格式
|
||||
if (str.startsWith('{') && str.endsWith('}')) {
|
||||
final content = str.substring(1, str.length - 1);
|
||||
final pairs = content.split(', ');
|
||||
|
||||
for (final pair in pairs) {
|
||||
final colonIndex = pair.indexOf(': ');
|
||||
if (colonIndex > 0) {
|
||||
final key = pair.substring(0, colonIndex).replaceAll('"', '');
|
||||
var value = pair.substring(colonIndex + 2).replaceAll('"', '');
|
||||
|
||||
// 尝试转换数字
|
||||
if (int.tryParse(value) != null) {
|
||||
result[key] = int.parse(value);
|
||||
} else if (double.tryParse(value) != null) {
|
||||
result[key] = double.parse(value);
|
||||
} else if (value == 'true') {
|
||||
result[key] = true;
|
||||
} else if (value == 'false') {
|
||||
result[key] = false;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print('解析结果: $result');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 初始化题目列表(打乱顺序)
|
||||
Future<PoetryInitResult> initializeQuestions() async {
|
||||
try {
|
||||
// 检查网络状态
|
||||
_isOnline = await _checkNetworkStatus();
|
||||
final isOfflineSetting = await _checkOfflineSetting();
|
||||
|
||||
// 判断是否使用离线模式
|
||||
if (!_isOnline || isOfflineSetting) {
|
||||
_isOfflineMode = true;
|
||||
|
||||
// 检查是否有缓存数据
|
||||
final hasCache = await _hasCachedData();
|
||||
if (!hasCache) {
|
||||
return PoetryInitResult(
|
||||
success: false,
|
||||
message: '离线模式下无缓存数据,请先下载或检查网络连接',
|
||||
needDownload: true,
|
||||
);
|
||||
}
|
||||
|
||||
// 加载离线缓存
|
||||
await _loadOfflineCache();
|
||||
|
||||
if (_offlineQuestions.isEmpty) {
|
||||
return PoetryInitResult(
|
||||
success: false,
|
||||
message: '离线缓存数据为空,请先下载数据',
|
||||
needDownload: true,
|
||||
);
|
||||
}
|
||||
|
||||
return PoetryInitResult(
|
||||
success: true,
|
||||
message: '已加载离线缓存 ${_offlineQuestions.length} 条题目',
|
||||
isOffline: true,
|
||||
);
|
||||
}
|
||||
|
||||
// 在线模式
|
||||
_isOfflineMode = false;
|
||||
|
||||
// 先调用 fetch 获取新题
|
||||
final fetchResponse = await HttpClient.get(
|
||||
'poe/api.php',
|
||||
queryParameters: {'action': 'fetch'},
|
||||
);
|
||||
|
||||
if (fetchResponse.isSuccess) {
|
||||
final fetchData = fetchResponse.jsonData;
|
||||
if (fetchData['code'] == 0) {
|
||||
_total = fetchData['data']['total'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 再调用 refresh 刷新缓存
|
||||
final refreshResponse = await HttpClient.get(
|
||||
'poe/api.php',
|
||||
queryParameters: {'action': 'refresh'},
|
||||
);
|
||||
|
||||
if (refreshResponse.isSuccess) {
|
||||
final refreshData = refreshResponse.jsonData;
|
||||
if (refreshData['code'] == 0) {
|
||||
_total = refreshData['data']['total'] ?? _total;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成并打乱题目 ID 列表
|
||||
_generateShuffledIds();
|
||||
_currentIndex = 0;
|
||||
|
||||
return PoetryInitResult(
|
||||
success: true,
|
||||
message: '已加载在线题库 $_total 条题目',
|
||||
isOffline: false,
|
||||
);
|
||||
} catch (e) {
|
||||
print('初始化题目失败: $e');
|
||||
|
||||
// 尝试加载离线缓存
|
||||
final hasCache = await _hasCachedData();
|
||||
if (hasCache) {
|
||||
_isOfflineMode = true;
|
||||
await _loadOfflineCache();
|
||||
|
||||
if (_offlineQuestions.isNotEmpty) {
|
||||
return PoetryInitResult(
|
||||
success: true,
|
||||
message: '网络异常,已加载离线缓存 ${_offlineQuestions.length} 条题目',
|
||||
isOffline: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return PoetryInitResult(
|
||||
success: false,
|
||||
message: '初始化题目失败: $e',
|
||||
needDownload: !hasCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成并打乱题目 ID 列表
|
||||
void _generateShuffledIds() {
|
||||
_shuffledIds = List.generate(_total, (index) => index);
|
||||
_shuffleList(_shuffledIds);
|
||||
}
|
||||
|
||||
/// 打乱列表顺序(Fisher-Yates 算法)
|
||||
void _shuffleList(List<int> list) {
|
||||
for (int i = list.length - 1; i > 0; i--) {
|
||||
int j = _random.nextInt(i + 1);
|
||||
int temp = list[i];
|
||||
list[i] = list[j];
|
||||
list[j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下一题
|
||||
void nextQuestion() {
|
||||
if (_shuffledIds.isEmpty && _offlineQuestions.isEmpty) return;
|
||||
|
||||
_currentIndex++;
|
||||
if (_currentIndex >= _total) {
|
||||
_currentIndex = 0;
|
||||
// 循环时重新打乱顺序
|
||||
if (_shuffledIds.isNotEmpty) {
|
||||
_shuffleList(_shuffledIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一题
|
||||
void previousQuestion() {
|
||||
if (_shuffledIds.isEmpty && _offlineQuestions.isEmpty) return;
|
||||
|
||||
_currentIndex--;
|
||||
if (_currentIndex < 0) {
|
||||
_currentIndex = _total - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载题目
|
||||
Future<PoetryQuestionResult> loadQuestion() async {
|
||||
if (isNetworkLoading('load_question')) {
|
||||
return PoetryQuestionResult(success: false, message: '正在加载中...');
|
||||
}
|
||||
|
||||
if (_shuffledIds.isEmpty && _offlineQuestions.isEmpty) {
|
||||
return PoetryQuestionResult(success: false, message: '题目列表未初始化');
|
||||
}
|
||||
|
||||
// 离线模式:从缓存加载
|
||||
if (_isOfflineMode && _offlineQuestions.isNotEmpty) {
|
||||
final questionIndex = _shuffledIds[_currentIndex];
|
||||
if (questionIndex >= 0 && questionIndex < _offlineQuestions.length) {
|
||||
final questionData = _offlineQuestions[questionIndex];
|
||||
|
||||
// 构建标准格式的题目数据
|
||||
final formattedData = _formatOfflineQuestion(questionData);
|
||||
|
||||
return PoetryQuestionResult(
|
||||
success: true,
|
||||
data: formattedData,
|
||||
questionId: questionIndex,
|
||||
questionIndex: _currentIndex,
|
||||
isOffline: true,
|
||||
);
|
||||
}
|
||||
|
||||
return PoetryQuestionResult(success: false, message: '题目索引越界');
|
||||
}
|
||||
|
||||
startNetworkLoading('load_question');
|
||||
|
||||
try {
|
||||
final id = currentId;
|
||||
final response = await HttpClient.get(
|
||||
'poe/api.php',
|
||||
queryParameters: {'action': 'question', 'id': id.toString()},
|
||||
);
|
||||
|
||||
if (response.isSuccess) {
|
||||
final data = response.jsonData;
|
||||
if (data['code'] == 0) {
|
||||
final questionData = data['data'];
|
||||
return PoetryQuestionResult(
|
||||
success: true,
|
||||
data: questionData,
|
||||
questionId: id,
|
||||
questionIndex: _currentIndex,
|
||||
isOffline: false,
|
||||
);
|
||||
} else {
|
||||
return PoetryQuestionResult(
|
||||
success: false,
|
||||
message: data['msg'] ?? '获取题目失败',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return PoetryQuestionResult(
|
||||
success: false,
|
||||
message: '网络错误: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return PoetryQuestionResult(success: false, message: '加载失败: $e');
|
||||
} finally {
|
||||
endNetworkLoading('load_question');
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化离线题目数据
|
||||
Map<String, dynamic> _formatOfflineQuestion(Map<String, dynamic> data) {
|
||||
print('格式化离线题目,原始数据: $data');
|
||||
|
||||
// 检查是否已经是标准格式
|
||||
if (data.containsKey('question') && data.containsKey('options')) {
|
||||
final options = data['options'];
|
||||
print('发现options字段,类型: ${options.runtimeType}, 值: $options');
|
||||
// 确保options是List类型
|
||||
if (options is List) {
|
||||
print('options已经是List类型,直接返回');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从缓存数据中提取字段
|
||||
final result = <String, dynamic>{
|
||||
'id': data['id'] ?? 0,
|
||||
'question': data['question'] ?? data['question_content'] ?? '未知题目',
|
||||
'author': data['author'] ?? '未知作者',
|
||||
'type': data['type'] ?? '',
|
||||
'grade': data['grade'] ?? '',
|
||||
'dynasty': data['dynasty'] ?? '',
|
||||
'options': <Map<String, dynamic>>[],
|
||||
};
|
||||
|
||||
print('构建基础数据: $result');
|
||||
|
||||
// 尝试解析options字段
|
||||
dynamic optionsData = data['options'];
|
||||
if (optionsData != null) {
|
||||
print('开始解析options,类型: ${optionsData.runtimeType}');
|
||||
if (optionsData is List) {
|
||||
// 已经是List,直接使用
|
||||
result['options'] = optionsData;
|
||||
print('options是List,直接使用');
|
||||
} else if (optionsData is String) {
|
||||
// 是String,尝试解析为List
|
||||
try {
|
||||
print('options是String,尝试解析: $optionsData');
|
||||
final parsedOptions = jsonDecode(optionsData);
|
||||
print('解析结果类型: ${parsedOptions.runtimeType}');
|
||||
if (parsedOptions is List) {
|
||||
result['options'] = parsedOptions;
|
||||
print('options解析成功为List');
|
||||
}
|
||||
} catch (e) {
|
||||
print('解析options字符串失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print('当前options: ${result['options']}');
|
||||
|
||||
// 如果没有有效的options,尝试从其他字段构建
|
||||
if ((result['options'] as List).isEmpty) {
|
||||
print('options为空,尝试从其他字段构建');
|
||||
final options = <Map<String, dynamic>>[];
|
||||
for (int i = 1; i <= 4; i++) {
|
||||
final optionKey = 'option_$i';
|
||||
if (data.containsKey(optionKey)) {
|
||||
options.add({'index': i, 'content': data[optionKey]});
|
||||
print('从$optionKey构建选项');
|
||||
}
|
||||
}
|
||||
result['options'] = options;
|
||||
}
|
||||
|
||||
print('最终格式化结果: $result');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 提交答案
|
||||
Future<PoetryAnswerResult> submitAnswer(int questionId, int answer) async {
|
||||
// 离线模式:本地验证答案
|
||||
if (_isOfflineMode && _offlineQuestions.isNotEmpty) {
|
||||
if (questionId >= 0 && questionId < _offlineQuestions.length) {
|
||||
final questionData = _offlineQuestions[questionId];
|
||||
final correctAnswer =
|
||||
questionData['correct_answer'] ??
|
||||
questionData['answer'] ??
|
||||
questionData['correct'];
|
||||
|
||||
// 比较答案
|
||||
bool isCorrect = false;
|
||||
if (correctAnswer != null) {
|
||||
if (correctAnswer is int) {
|
||||
isCorrect = correctAnswer == answer;
|
||||
} else if (correctAnswer is String) {
|
||||
isCorrect = correctAnswer == answer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return PoetryAnswerResult(
|
||||
success: true,
|
||||
message: isCorrect ? '回答正确!' : '回答错误',
|
||||
isCorrect: isCorrect,
|
||||
nextQuestion: null,
|
||||
);
|
||||
}
|
||||
|
||||
return PoetryAnswerResult(success: false, message: '题目不存在');
|
||||
}
|
||||
|
||||
startNetworkLoading('submit_answer');
|
||||
|
||||
try {
|
||||
final response = await HttpClient.get(
|
||||
'poe/api.php',
|
||||
queryParameters: {
|
||||
'action': 'answer',
|
||||
'id': questionId.toString(),
|
||||
'answer': answer.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.isSuccess) {
|
||||
final data = response.jsonData;
|
||||
if (data['code'] == 0) {
|
||||
return PoetryAnswerResult(
|
||||
success: true,
|
||||
message: data['msg'],
|
||||
isCorrect: data['data']['correct'] == true,
|
||||
nextQuestion: data['data']['has_next'] == true
|
||||
? {'id': data['data']['next_id']}
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
return PoetryAnswerResult(
|
||||
success: false,
|
||||
message: data['msg'] ?? '提交失败',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return PoetryAnswerResult(
|
||||
success: false,
|
||||
message: '网络错误: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return PoetryAnswerResult(success: false, message: '提交失败: $e');
|
||||
} finally {
|
||||
endNetworkLoading('submit_answer');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取提示
|
||||
Future<PoetryHintResult> getHint(int questionId) async {
|
||||
// 离线模式:从缓存数据获取提示
|
||||
if (_isOfflineMode && _offlineQuestions.isNotEmpty) {
|
||||
if (questionId >= 0 && questionId < _offlineQuestions.length) {
|
||||
final questionData = _offlineQuestions[questionId];
|
||||
final hint =
|
||||
questionData['hint'] ??
|
||||
'作者: ${questionData['author'] ?? '未知'},朝代: ${questionData['dynasty'] ?? '未知'}';
|
||||
|
||||
return PoetryHintResult(success: true, message: hint);
|
||||
}
|
||||
|
||||
return PoetryHintResult(success: false, message: '题目不存在');
|
||||
}
|
||||
|
||||
startNetworkLoading('get_hint');
|
||||
|
||||
try {
|
||||
final response = await HttpClient.get(
|
||||
'poe/api.php',
|
||||
queryParameters: {'action': 'hint', 'id': questionId.toString()},
|
||||
);
|
||||
|
||||
if (response.isSuccess) {
|
||||
final data = response.jsonData;
|
||||
if (data['code'] == 0) {
|
||||
return PoetryHintResult(success: true, message: data['data']['hint']);
|
||||
} else {
|
||||
return PoetryHintResult(
|
||||
success: false,
|
||||
message: data['msg'] ?? '获取提示失败',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return PoetryHintResult(
|
||||
success: false,
|
||||
message: '网络错误: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return PoetryHintResult(success: false, message: '获取提示失败: $e');
|
||||
} finally {
|
||||
endNetworkLoading('get_hint');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化结果
|
||||
class PoetryInitResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final bool isOffline;
|
||||
final bool needDownload;
|
||||
|
||||
PoetryInitResult({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.isOffline = false,
|
||||
this.needDownload = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// 题目加载结果
|
||||
class PoetryQuestionResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final Map<String, dynamic>? data;
|
||||
final int questionId;
|
||||
final int questionIndex;
|
||||
final bool isOffline;
|
||||
|
||||
PoetryQuestionResult({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.questionId = 0,
|
||||
this.questionIndex = 0,
|
||||
this.isOffline = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// 答案提交结果
|
||||
class PoetryAnswerResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final bool isCorrect;
|
||||
final Map<String, dynamic>? nextQuestion;
|
||||
|
||||
PoetryAnswerResult({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.isCorrect = false,
|
||||
this.nextQuestion,
|
||||
});
|
||||
}
|
||||
|
||||
/// 提示获取结果
|
||||
class PoetryHintResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
|
||||
PoetryHintResult({required this.success, this.message});
|
||||
}
|
||||
Reference in New Issue
Block a user