614 lines
17 KiB
Dart
614 lines
17 KiB
Dart
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});
|
||
}
|