This commit is contained in:
chenshuainan
2026-09-28 20:12:34 +08:00
parent 847986f861
commit 5f53bd8f94
60 changed files with 14624 additions and 398 deletions
@@ -0,0 +1,816 @@
/// ============================================================
/// 【课堂习题 · 单题提交 / 失败补提 / 交卷】
///
/// 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;
}
}