Files
2026-09-28 20:12:34 +08:00

817 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 【课堂习题 · 单题提交 / 失败补提 / 交卷】
///
/// 1:1 迁移 Vue exerciseSubmit.js(createSubmitHandlers)。
///
/// 职责:
/// 1) 单题提交(客观题 QuestionRenewTime / 主观题 PushAIGrading)
/// 2) 填空题(输入框 / 图片作答)统一处理与提交
/// 3) 批量统计未处理 / 未作答题目(batchProcessQuestions)
/// 4) 交卷弹窗与提交入口(submitExamPaper / handlePopupConfirm)
/// 5) 失败队列本地持久化(shared_preferences,key classExercisesDraft:$courseId)
/// ============================================================
library;
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:new_learn/core/components/upload.dart';
import 'package:new_learn/core/api/conquerPackage/classExercises/api.dart';
import 'package:new_learn/core/utils/question/exam_answer_card.dart';
import 'package:new_learn/core/utils/question/question_parser.dart';
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_session.dart';
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_timing.dart';
import 'package:new_learn/features/conquerPackage/classExercises/models/question_model.dart';
/// 失败补提结果(对标 Vue {success, failedCount})
class RetryResult {
const RetryResult({required this.success, required this.failedCount});
final bool success;
final int failedCount;
}
/// 答题卡额外计数
class AnswerCardExtraCounts {
const AnswerCardExtraCounts({
this.unansweredCount = 0,
this.pendingNotesCount = 0,
});
final int unansweredCount;
final int pendingNotesCount;
}
class _FailedEntry {
_FailedEntry({
required this.key,
required this.submitType,
required this.payload,
});
final String key;
final String submitType; // QuestionRenewTime / PushAIGrading
final Map<String, dynamic> payload;
}
class ExerciseSubmitHandlers {
ExerciseSubmitHandlers({
required this.session,
required this.bridge,
});
final ExerciseSession session;
final ExerciseUiBridge bridge;
final ClassExercisesApi _api = ClassExercisesApi.instance;
bool _submitLock = false;
Timer? _submitLockTimer;
/// 单题提交失败队列(交卷前统一补提)
final Map<String, _FailedEntry> _failedSubmitRequestMap = {};
String get _pendingStorageKey =>
'classExercisesDraft:${session.courseId}';
// ---------------- 失败队列 ----------------
String _buildFailedKey(Map<String, dynamic> payload) {
final id = payload['id'] ?? '';
final childId = payload['childId'] ?? 0;
final index = payload['index'] ?? '';
return '$id|$childId|$index';
}
void _upsertFailedSubmitRequest(
String submitType,
Map<String, dynamic> payload,
) {
final key = _buildFailedKey(payload);
_failedSubmitRequestMap[key] = _FailedEntry(
key: key,
submitType: submitType,
payload: Map<String, dynamic>.from(payload),
);
}
void _removeFailedSubmitRequest(
String submitType,
Map<String, dynamic> payload,
) {
_failedSubmitRequestMap.remove(_buildFailedKey(payload));
}
/// 进入页面时从本地缓存恢复失败队列
Future<void> restorePendingFromStorage() async {
final key = _pendingStorageKey;
if (key.endsWith(':')) return;
_failedSubmitRequestMap.clear();
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(key);
if (raw == null || raw.isEmpty) return;
final cached = jsonDecode(raw);
final items = cached is Map && cached['items'] is List
? cached['items'] as List
: const [];
for (final rawItem in items) {
if (rawItem is! Map) continue;
final payload = rawItem['payload'];
if (payload is! Map) continue;
final submitType = '${rawItem['submitType'] ?? ''}';
if (submitType != 'QuestionRenewTime' &&
submitType != 'PushAIGrading') {
continue;
}
final payloadMap = Map<String, dynamic>.from(payload);
final failedKey = _buildFailedKey(payloadMap);
_failedSubmitRequestMap[failedKey] = _FailedEntry(
key: failedKey,
submitType: submitType,
payload: payloadMap,
);
}
} catch (_) {
// 缓存损坏:忽略,从空队列开始
}
}
/// 把当前失败队列写回本地缓存(空队列时清缓存)
Future<void> persistPendingToStorage() async {
final key = _pendingStorageKey;
if (key.endsWith(':')) return;
try {
final prefs = await SharedPreferences.getInstance();
final entries = _failedSubmitRequestMap.values.toList();
if (entries.isEmpty) {
await prefs.remove(key);
return;
}
final items = entries
.map((e) => {
'key': e.key,
'submitType': e.submitType,
'payload': e.payload,
})
.toList();
await prefs.setString(
key,
jsonEncode({
'version': 1,
'updatedAt': DateTime.now().millisecondsSinceEpoch,
'courseId': session.courseId,
'items': items,
}),
);
} catch (_) {
// 落盘失败不阻塞流程
}
}
/// 补提失败队列,默认 3 轮
Future<RetryResult> retryFailedSubmitRequests({
int maxRetryRounds = 3,
}) async {
for (var round = 0; round < maxRetryRounds; round++) {
final currentFailedList = _failedSubmitRequestMap.values.toList();
if (currentFailedList.isEmpty) {
return const RetryResult(success: true, failedCount: 0);
}
for (final item in currentFailedList) {
try {
final safePayload =
normalizeSubmitTimeRange(Map<String, dynamic>.from(item.payload));
if (item.submitType == 'QuestionRenewTime') {
await _api.questionRenewTime(safePayload);
} else if (item.submitType == 'PushAIGrading') {
await _api.pushAIGrading(safePayload);
}
_failedSubmitRequestMap.remove(item.key);
} catch (_) {
_upsertFailedSubmitRequest(item.submitType, item.payload);
}
}
}
return RetryResult(
success: _failedSubmitRequestMap.isEmpty,
failedCount: _failedSubmitRequestMap.length,
);
}
int getPendingCount() => _failedSubmitRequestMap.length;
// ---------------- 答案回填 ----------------
/// 倒计时结束:仅处理当前可见题
void finishPlanTime(dynamic questionId) {
final current = session.currentQuestion;
final currentQuestionId = current?.id ?? '${session.currentIndex}';
if (questionId != null &&
'$questionId'.isNotEmpty &&
'$questionId' != currentQuestionId) {
return;
}
current?.m['ISFinishPlanTime'] = true;
session.countDownKey?.currentState?.reset();
}
/// 客观题选择答案(e 为含 userAnswer 的回调载荷)
void selectAnswer(dynamic userAnswer, [int? questionIndex]) {
final idx = questionIndex ?? session.currentIndex;
final current = idx >= 0 && idx < session.questions.length
? session.questions[idx]
: null;
if (current != null) current.userAnswer = userAnswer;
}
/// 单题答案更新(文本 / 图片 JSON 字符串)
void updateAnswer(dynamic value, [int? questionIndex]) {
final idx = questionIndex ?? session.currentIndex;
final current = idx >= 0 && idx < session.questions.length
? session.questions[idx]
: null;
if (current != null) current.userAnswer = value;
}
/// 复合题:子题作答内容回填(startTime / userAnswer)
void getMyMinAnswer({required int index, required QuestionModel data}) {
final current = session.currentQuestion;
final subs = current?.mapList('subQuestions') ?? const [];
if (index >= 0 && index < subs.length) {
subs[index]['startTime'] = data.m['startTime'];
subs[index]['userAnswer'] = data.m['userAnswer'];
}
}
/// 切题 / 切子题前,把当前倒计时剩余秒数写回题目
void calculateRemainTime() {
final info = session.currentQuestion;
if (info == null) return;
if (info.planTimeSecond > 0 && !info.isFinishPlanTime) {
final countdown = session.countDownKey?.currentState;
if (countdown == null) return;
final remainMs = countdown.getRemainTime();
final remain = (remainMs < 0 ? 0 : remainMs) ~/ 1000;
info.newPlanTimeSecond = remain;
}
}
/// 填空题(图片作答):subQuestions / childs 双写当前空的图片 JSON
void fillBlankImgChange(
List<dynamic> images,
int index, [
int? questionIndex,
]) {
final idx = questionIndex ?? session.currentIndex;
if (idx < 0 || idx >= session.questions.length) return;
final res = session.questions[idx];
final subs = res.mapList('subQuestions');
final childs = res.mapList('childs');
final value = jsonEncode(images);
if (index >= 0 && index < subs.length) {
subs[index]['userAnswer'] = value;
}
if (index >= 0 && index < childs.length) {
childs[index]['userAnswer'] = value;
}
}
// ---------------- 时间范围 / 提交分发 ----------------
/// endTime 不能早于 startTime(对标 normalizeSubmitTimeRange)
Map<String, dynamic> normalizeSubmitTimeRange(
Map<String, dynamic> queryData,
) {
final qd = Map<String, dynamic>.from(queryData);
if (qd['startTime'] == null || '${qd['startTime']}'.isEmpty) {
qd['startTime'] = getCurrentISO();
}
if (qd['endTime'] == null || '${qd['endTime']}'.isEmpty) {
qd['endTime'] = getCurrentISO();
}
final startRaw = qd['startTime'];
final endRaw = qd['endTime'];
if (startRaw == null || endRaw == null) return qd;
final startMs = DateTime.tryParse('$startRaw')?.millisecondsSinceEpoch;
final endMs = DateTime.tryParse('$endRaw')?.millisecondsSinceEpoch;
if (startMs == null || endMs == null) return qd;
if (endMs <= startMs) {
qd['endTime'] =
DateTime.fromMillisecondsSinceEpoch(startMs + 1000, isUtc: true)
.toIso8601String();
}
return qd;
}
/// 提交分发:userAnswer+answerStatus 走 RenewTime;answerImgs 走 PushAIGrading
Future<void> submitQuestionsReview(
Map<String, dynamic> queryData, {
bool throwOnError = false,
}) async {
if ('${queryData['id'] ?? ''}'.isEmpty) return;
final safeQueryData = normalizeSubmitTimeRange(queryData);
if (safeQueryData['startTime'] == null ||
safeQueryData['endTime'] == null) {
return;
}
if (queryData.containsKey('userAnswer') &&
queryData.containsKey('answerStatus')) {
try {
await _api.questionRenewTime(safeQueryData);
_removeFailedSubmitRequest('QuestionRenewTime', safeQueryData);
} catch (err) {
_upsertFailedSubmitRequest('QuestionRenewTime', safeQueryData);
if (throwOnError) rethrow;
}
}
if (queryData.containsKey('answerImgs')) {
try {
await _api.pushAIGrading(safeQueryData);
_removeFailedSubmitRequest('PushAIGrading', safeQueryData);
} catch (err) {
_upsertFailedSubmitRequest('PushAIGrading', safeQueryData);
if (throwOnError) rethrow;
}
}
}
/// AI 判题提交(主观题 / 图片作答)
/// [id] 非空表示子题提交(id=父题 id,childId=子题 id)
Future<void> pushAIGradingFn(
QuestionModel data,
String? id, {
bool throwOnError = false,
}) async {
session.processedQuestionIds.add(data.id);
List<dynamic> answerImgs;
if (id != null && id.isNotEmpty) {
final userAnswer = data.userAnswer;
final List<dynamic> parsed = userAnswer.isEmpty
? const []
: (jsonDecode(userAnswer) as List? ?? const []);
answerImgs = parsed.whereType<Map>().map((raw) {
return {
'url': raw['Url'] ?? raw['url'],
'thumbnailUrl': raw['ThumbnailUrl'] ?? raw['thumbnailUrl'],
};
}).toList();
} else {
final pageState = session.itemRefs[session.currentIndex]?.currentState;
final rawList = pageState?.getImageList('uploadAnswerRef') ?? const [];
answerImgs = _normalizeAnswerImgs(rawList);
}
final queryData = <String, dynamic>{
'id': (id != null && id.isNotEmpty) ? id : data.id,
'childId': (id != null && id.isNotEmpty) ? data.id : 0,
'courseId': session.courseId,
'startTime': data.m['startTime'],
'endTime': getCurrentISO(),
'answerImgs': answerImgs,
};
await submitQuestionsReview(
queryData,
throwOnError: throwOnError,
);
}
List<Map<String, dynamic>> _normalizeAnswerImgs(List<dynamic> rawList) {
final result = <Map<String, dynamic>>[];
for (final raw in rawList) {
if (raw is UploadItem) {
if (raw.url.isEmpty) continue;
result.add({
'url': raw.url,
'thumbnailUrl': raw.thumbnailUrl ?? raw.url,
});
} else if (raw is Map) {
final url = '${raw['Url'] ?? raw['url'] ?? ''}';
if (url.isEmpty) continue;
result.add({
'url': url,
'thumbnailUrl': '${raw['ThumbnailUrl'] ?? raw['thumbnailUrl'] ?? url}',
});
}
}
return result;
}
/// 填空题图片作答提交(单空 / 多空统一走 answerImgs)
Future<void> handleFillBlankAnswer(
QuestionModel data,
String? id, {
bool throwOnError = false,
}) async {
final userAnswer = data.userAnswer;
final answerImgs = userAnswer.isEmpty
? const []
: (jsonDecode(userAnswer) as List? ?? const []);
final queryData = <String, dynamic>{
'id': (id != null && id.isNotEmpty) ? id : data.id,
'childId': 0,
'courseId': session.courseId,
'startTime': data.m['startTime'],
'endTime': getCurrentISO(),
'answerImgs': answerImgs,
};
await submitQuestionsReview(
queryData,
throwOnError: throwOnError,
);
}
/// 题目提交入口(对标 QuestionRenewTimeFn)
Future<void> questionRenewTimeFn(
QuestionModel data,
String? id, {
bool throwOnError = false,
}) async {
if (session.isView) return;
session.processedQuestionIds.add(data.id);
// 非客观题 非填空 → AI 判题
if (data.options.isEmpty && data.questionTypes != 'tiankongti') {
await pushAIGradingFn(data, id, throwOnError: throwOnError);
return;
}
// 填空题(图片作答)
if (data.isFillblankItemUpload == 1 &&
data.questionTypes == 'tiankongti') {
await handleFillBlankAnswer(data, id, throwOnError: throwOnError);
return;
}
// 客观题:对比前剥离 HTML(与 Vue 一致,直接回写题目数据)
data.correctAnswer = stripAnswerPlain(data.correctAnswer);
data.userAnswer = stripAnswerPlain(data.userAnswer);
var answerStatus = 0;
if (formatQuestionText(data.userAnswer) ==
formatQuestionText(data.correctAnswer)) {
answerStatus = 10;
}
if (formatQuestionText(data.userAnswer) !=
formatQuestionText(data.correctAnswer)) {
answerStatus = 11;
}
// 多选题必须完全匹配;其他题型保留“包含即对”
if (data.questionTypes != 'duoxuanti' &&
data.correctAnswer.contains(data.userAnswer) &&
data.options.isNotEmpty) {
answerStatus = 10;
}
if ('${data.m['userAnswer'] ?? ''}'.isEmpty) answerStatus = 0;
final queryData = <String, dynamic>{
'id': (id != null && id.isNotEmpty) ? id : data.id,
'childId': (id != null && id.isNotEmpty) ? data.id : 0,
'courseId': session.courseId,
'userAnswer': data.userAnswer,
'startTime': data.m['startTime'],
'endTime': getCurrentISO(),
'answerStatus': answerStatus,
};
// 七选五
if (data.questionTypes == 'qixuanwu_sub' ||
data.questionTypes == 'qixuanwu') {
queryData['childId'] = 0;
queryData['index'] = data.m['index'];
final plainCorrect = stripAnswerPlain(data.correctAnswer);
final plainUser = stripAnswerPlain(data.userAnswer);
queryData['answerStatus'] = plainCorrect.contains(plainUser) ? 10 : 11;
if ('${data.m['userAnswer'] ?? ''}'.isEmpty) {
queryData['answerStatus'] = 0;
}
}
// 填空输入类:走小题
if (data.isFillblankItemUpload == 0 &&
data.questionTypes == 'tiankongti') {
queryData['childId'] = 0;
queryData['index'] = data.m['index'] ?? 1;
queryData['answerStatus'] = formatQuestionText(data.correctAnswer) ==
formatQuestionText(data.userAnswer)
? 10
: 11;
if ('${data.m['userAnswer'] ?? ''}'.isEmpty) {
queryData['answerStatus'] = 0;
}
}
await submitQuestionsReview(
queryData,
throwOnError: throwOnError,
);
}
/// 复合题切子题时回填并提交(对标 seleCthildData)
void selectChildData({required int index, required QuestionModel data}) {
final currentItem = session.currentQuestion;
final subs = currentItem?.mapList('subQuestions') ?? const [];
var submitData = QuestionModel(<String, dynamic>{});
if (index >= 0 && index < subs.length) {
final sub = subs[index];
sub['startTime'] = data.m['startTime'];
sub['userAnswer'] = data.m['userAnswer'];
submitData = QuestionModel(sub);
}
final parentId = session.currentQuestion?.id ?? '';
questionRenewTimeFn(submitData, parentId);
}
// ---------------- 答题卡统计 ----------------
/// 拍平成答题卡题号列表(默认 isExamAnswerCard=true)
List<Map<String, dynamic>> batchProcessQuestions({
bool isExamAnswerCard = true,
}) {
return buildExamAnswerCardQuestions(
session.questions.map((q) => q.m).toList(),
isExamAnswerCard: isExamAnswerCard,
);
}
/// 未作答 / 待补笔记计数(口径与 Vue 一致)
AnswerCardExtraCounts getAnswerCardExtraCounts({
bool isExamAnswerCard = true,
}) {
final list = batchProcessQuestions(isExamAnswerCard: isExamAnswerCard);
var unansweredCount = 0;
var pendingNotesCount = 0;
for (final res in list) {
final userAnswer = res['userAnswer'];
final answerImgs = res['answerImgs'];
final answered = userAnswer != null && '$userAnswer'.isNotEmpty ||
(answerImgs is List && answerImgs.isNotEmpty);
if (!answered) unansweredCount += 1;
if (res['isUploadNotes'] == true) {
final notes = res['notes'];
if (!(notes is List && notes.isNotEmpty)) pendingNotesCount += 1;
}
}
return AnswerCardExtraCounts(
unansweredCount: unansweredCount,
pendingNotesCount: pendingNotesCount,
);
}
// ---------------- 弹窗配置 ----------------
/// 构建并展示攻克流程弹窗(对标 getConquerPopupCon)
void getConquerPopupCon(
String type,
dynamic data, {
String? confirmText,
String? cancelText,
bool? isConfirm,
String? confirmAction,
String? title,
String? subtitle,
}) {
late ConquerPopupConfig config;
if (type == 'note') {
config = ConquerPopupConfig(
show: true,
type: 'note',
title: '本题建议上传过程笔记',
subtitle: '记录解题思维过程,有助于您更好地复盘错题,提升学习效率。',
confirmText: '上传笔记',
cancelText: '取消',
);
} else if (type == 'grading') {
config = ConquerPopupConfig(
show: true,
type: 'grading',
title: '题目批改中',
subtitle: '预计 3~5 分钟有结果',
confirmText: '返回攻克列表',
cancelText: '取消',
);
} else if (type == 'submit') {
config = ConquerPopupConfig(
show: true,
type: 'submit',
title: '确认提交?',
subtitle:
'您还有<span class="spans" style="font-size:32rpx;color:#3D7DF5;"> $data</span> 道题未完成,是否确定提交练习?',
confirmText: '提交',
cancelText: '取消',
);
} else {
// time
final isPad = bridge.isPad;
config = ConquerPopupConfig(
show: true,
type: 'time',
title:
"<div style='font-size:${isPad ? '16px' : '14px'};'>根据你过往数据,建议攻克$data</div>",
subtitle:
"<div style=\"font-size:${isPad ? '13px' : '11px'};\">仓促速学掌握不牢,之后还要重新复习</div>"
"<div style=\"font-size:${isPad ? '13px' : '11px'};\">本次攻克成功,学习时长会逐步减少</div>",
confirmText: '跳过',
cancelText: '继续作答',
confirmAction: 'minimumLearningDuration',
);
}
// con 覆盖项
if (confirmText != null) config.confirmText = confirmText;
if (cancelText != null) config.cancelText = cancelText;
if (isConfirm != null) config.isConfirm = isConfirm;
if (confirmAction != null) config.confirmAction = confirmAction;
if (title != null) config.title = title;
if (subtitle != null) config.subtitle = subtitle;
bridge.showConquerPopup(config);
}
// ---------------- 交卷 ----------------
Future<void> _submitCurrentQuestionBeforeSubmitExamPaper() async {
if (session.isView) return;
final item = session.currentQuestion;
if (item == null) return;
// 填空题单个空:直接提交大题
if (item.childs.length == 1 && item.questionTypes == 'tiankongti') {
await questionRenewTimeFn(item, null, throwOnError: true);
return;
}
// hasOptionChildrenQuestion=false 且图片作答:整题算 1 题
if (item.isFillblankItemUpload == 1 &&
!item.hasOptionChildrenQuestion) {
await questionRenewTimeFn(item, null, throwOnError: true);
return;
}
// 有小题:提交当前激活子题
final hasSub = shouldCountSubQuestionsLocal(item) || item.childs.isNotEmpty;
if (hasSub) {
final childData = session.panelKey?.currentState?.getChildData();
await questionRenewTimeFn(
childData?.data ?? QuestionModel(<String, dynamic>{}),
item.id,
throwOnError: true,
);
return;
}
await questionRenewTimeFn(item, null, throwOnError: true);
}
bool shouldCountSubQuestionsLocal(QuestionModel item) =>
item.hasOptionChildrenQuestion && item.subQuestions.isNotEmpty;
/// 交卷入口(type=='submit' 表示用户已确认,跳过未答确认弹窗)
Future<void> submitExamPaper(String type) async {
if (_submitLock) return;
_submitLock = true;
_submitLockTimer?.cancel();
_submitLockTimer = Timer(const Duration(milliseconds: 18000), () {
_submitLock = false;
_submitLockTimer = null;
});
bridge.showLoading('提交中...');
try {
await _submitCurrentQuestionBeforeSubmitExamPaper();
final counts = getAnswerCardExtraCounts();
if (counts.unansweredCount > 0) {
bridge.setAnswerSheetVisible(false);
getConquerPopupCon(
'submit',
counts.unansweredCount,
confirmText: '确定',
isConfirm: true,
confirmAction: 'showAnswerSheet',
title: '暂时无法提交',
subtitle:
'您还有<span class="spans" style="font-size:32rpx;color:#3D7DF5;"> ${counts.unansweredCount}</span> 道题未作答',
);
return;
}
if (counts.pendingNotesCount > 0) {
bridge.setAnswerSheetVisible(false);
getConquerPopupCon(
'submit',
counts.pendingNotesCount,
confirmText: '确定',
isConfirm: true,
confirmAction: 'showAnswerSheet',
title: '暂时无法提交',
subtitle:
'您还有<span class="spans" style="font-size:32rpx;color:#3D7DF5;"> ${counts.pendingNotesCount}</span> 道题待补笔记',
);
return;
}
final unprocessedList = batchProcessQuestions();
final noAnswer = unprocessedList.where((res) {
final userAnswer = res['userAnswer'];
final answerImgs = res['answerImgs'];
return (userAnswer == null || '$userAnswer'.isEmpty) &&
!(answerImgs is List && answerImgs.isNotEmpty);
}).toList();
if (!(noAnswer.isEmpty || type == 'submit')) {
bridge.setAnswerSheetVisible(false);
getConquerPopupCon('submit', noAnswer.length);
return;
}
if (_failedSubmitRequestMap.isNotEmpty) {
bridge.showLoading('提交中...');
}
final retryResult = await retryFailedSubmitRequests();
if (!retryResult.success) {
bridge.hideLoading();
bridge.toast('网络异常,仍有 ${retryResult.failedCount} 道题提交失败,请稍后重试');
await Future<void>.delayed(const Duration(milliseconds: 1500));
return;
}
await Future<void>.delayed(const Duration(milliseconds: 2000));
bridge.showLoading('提交中...');
bridge.hideLoading();
return;
final res = await _api.recordSubmit(session.recordId);
bridge.setAnswerSheetVisible(false);
if (_field(res, 'code') == -1 || _field(res, 'code') == '-1') {
bridge.hideLoading();
final message = '${_field(res, 'message') ?? ''}';
Future<void>.delayed(const Duration(milliseconds: 500), () {
if (message.isNotEmpty) bridge.toast(message);
});
return;
}
final aiGradingCount = _field(res, 'aiGradingCount');
if (aiGradingCount != null &&
aiGradingCount != 0 &&
'$aiGradingCount' != '0') {
getConquerPopupCon('grading', null);
} else {
bridge.redirectToReport(session.recordId, session.packId);
}
} catch (_) {
bridge.hideLoading();
Future<void>.delayed(const Duration(milliseconds: 500), () {
bridge.toast('网络请求失败,请检查网络');
});
} finally {
_submitLockTimer?.cancel();
_submitLockTimer = null;
bridge.hideLoading();
_submitLock = false;
}
}
dynamic _field(dynamic resp, String key) {
if (resp is Map) {
if (resp[key] != null) return resp[key];
final data = resp['data'];
if (data is Map && data[key] != null) return data[key];
}
return null;
}
/// 弹窗确认按钮分发(对标 handlePopupConfirm)
void handlePopupConfirm(ConquerPopupConfig config) {
if (config.type == 'grading') {
bridge.navigateBack(delta: 3);
} else if (config.type == 'submit') {
if (config.confirmAction == 'showAnswerSheet') {
bridge.hideConquerPopup();
bridge.setAnswerSheetVisible(true);
return;
}
submitExamPaper('submit');
} else if (config.type == 'time' || config.type == 'note') {
bridge.hideConquerPopup();
}
}
/// 最低作答时长提示(返回 true 表示拦截跳题/交卷)
bool openTimePopupIfNeeded() {
final info = session.currentQuestion;
if (info == null) return false;
if (!session.isView &&
info.planTimeSecond > 0 &&
!info.isFinishPlanTime) {
getConquerPopupCon('time', formatTime(info.planTimeSecond));
return true;
}
return false;
}
/// 上传笔记提示(返回 true 表示拦截跳题/交卷)
bool openUploadNotesPopupIfNeeded() {
final info = session.currentQuestion;
if (info != null && info.isUploadNotes && info.notes.isEmpty) {
getConquerPopupCon('note', null);
return true;
}
return false;
}
}