// lib/pages/study_take_exam_page.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/modules/edu_api.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/services/SessionService.dart'; import 'dart:convert'; import 'package:qhd_prevention/tools/tools.dart'; class Question { final String questionDry; final String questionType; final String questionId; final String answer; // 正确答案(如果接口返回) final double score; final Map options; // 用户选择的答案(UI 中变化) String choiceAnswer; Question({ required this.questionDry, required this.questionType, required this.questionId, required this.answer, required this.score, required this.options, this.choiceAnswer = '', }); factory Question.fromJson(Map json) { final type = '${json['questionType'] ?? ''}'; final opts = {}; if (type == '1' || type == '2' || type == '3') { opts['A'] = json['optionA'] as String? ?? ''; opts['B'] = json['optionB'] as String? ?? ''; if (type != '3') { opts['C'] = json['optionC'] as String? ?? ''; opts['D'] = json['optionD'] as String? ?? ''; } } // 保留原接口回传的参考答案(若有) final answer = json['answer'] as String? ?? ''; final qDry = json['questionDry'] as String? ?? ''; final qId = json['questionId'] as String? ?? ''; double score = json['score']; return Question( questionDry: qDry, questionType: type, questionId: qId, answer: answer, score: score, options: opts, ); } /// 构造提交给后端的最小字段对象 Map toSubmitJson() { return { 'questionId': questionId, 'answer': answer, 'choiceAnswer': choiceAnswer, 'score': score, }; } } class StudyTakeExamPage extends StatefulWidget { const StudyTakeExamPage({ required this.examInfo, required this.signInfo, super.key, }); final Map examInfo; final Map signInfo; @override State createState() => _StudyTakeExamPageState(); } class _StudyTakeExamPageState extends State { late final List questions; late final Map info; int current = 0; late int remainingSeconds; Timer? _timer; final String _startExamTime = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); final questionTypeMap = { '1': '单选题', '2': '多选题', '3': '判断题', '4': '填空题', }; @override void initState() { super.initState(); info = widget.examInfo as Map? ?? {}; final rawList = widget.examInfo['questionList'] as List? ?? []; questions = rawList.map((e) => Question.fromJson(e as Map)).toList(); final minutes = info['examTime'] as int? ?? 0; remainingSeconds = minutes * 60; _startTimer(); } @override void dispose() { _timer?.cancel(); super.dispose(); } Future _showTip(String content) { return CustomAlertDialog.showAlert( context, title: '温馨提示', content: content, confirmText: '确认', ); } bool _validateCurrentAnswer() { final q = questions[current]; if (q.choiceAnswer.isEmpty) { _showTip('请对本题进行作答。'); return false; } if (q.questionType == '2' && q.choiceAnswer.split(',').length < 2) { _showTip('多选题最少需要选择两个答案。'); return false; } return true; } void _startTimer() { _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (remainingSeconds <= 0) { timer.cancel(); _onTimeUp(); } else { setState(() => remainingSeconds--); } }); } /// 重置页面以便“继续考试” void _resetForRetry() { // 清空每题的选择 for (var q in questions) { q.choiceAnswer = ''; } // 重置当前题索引、计时等 setState(() { current = 0; final minutes = info['examTime'] as int? ?? 0; remainingSeconds = minutes * 60; }); // 重新启动计时器 _startTimer(); } void _chooseTopic(String type, String key) { final q = questions[current]; if (type == 'radio' || type == 'judge') { q.choiceAnswer = (q.choiceAnswer == key) ? '' : key; } else { final chars = q.choiceAnswer.isEmpty ? [] : q.choiceAnswer.split(','); if (chars.contains(key)) { chars.remove(key); } else { chars.add(key); } chars.sort(); q.choiceAnswer = chars.join(','); } setState(() {}); } void _nextQuestion() { if (!_validateCurrentAnswer()) return; if (current < questions.length - 1) setState(() => current++); } void _previousQuestion() { if (current > 0) setState(() => current--); } void _confirmSubmit() async { if (!_validateCurrentAnswer()) return; final ok = await CustomAlertDialog.showConfirm( context, title: '温馨提示', content: '请确认是否交卷!', cancelText: '取消', ); if (ok) { _submit(); } } Future _submit() async { // 按原逻辑:若多选题需要去掉逗号(保留你的业务要求) for (var q in questions) { if (q.questionType == '2') { // 如果后端期待无逗号格式的话保留这一步,否则可删 q.choiceAnswer = q.choiceAnswer.replaceAll(',', ''); } } // 构造真正的 JSON 数组(而不是 JSON 字符串) final questionList = questions.map((q) => q.toSubmitJson()).toList(); final data = { 'studentId': widget.signInfo['studentId'] ?? '', 'classId': widget.examInfo['classId'] ?? '', 'corpinfoId': SessionService.instance.tenantId ?? '', 'classExamPaperId': widget.examInfo['classExamPaperId'] ?? '', 'examPaperId': widget.examInfo['examPaperId'] ?? '', 'studentSignId': widget.signInfo['studentSignId'], 'examTimeBegin': _startExamTime, // 当前时间就是结束时间 'examTimeEnd': DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), 'questionList': questionList, // 这里放真正的 List> }; // final r = await CustomAlertDialog.showConfirm(context, title: '参数', content: jsonEncode(data)); // if (!r) return; LoadingDialogHelper.show(message: '正在提交'); final res = await EduApi.submitExam(data); LoadingDialogHelper.hide(); if (res['success']) { final data = res['data'] as Map? ?? {}; final score = data['examScore'] ?? 0; final passed = data['result'] == 1; // 弹窗告诉用户结果:通过直接返回上一页;未通过给“继续考试 / 确定”两个按钮 final result = await CustomAlertDialog.showConfirm( context, title: '温馨提示', content: passed ? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!' : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!', cancelText: passed ? '' : '继续考试', confirmText: '确定', ); // 如果考试通过或用户点击了“确定”,则返回上一页 if (passed || result) { Navigator.of(context).pop(); return; } // 到这里说明:未通过且用户点击了“继续考试” -> 重置页面状态并继续考试 _resetForRetry(); } else { // 可选:处理失败情况并提示错误信息 final msg = res['message'] ?? '提交失败,请重试'; ToastUtil.showError(context, msg); } } void _onTimeUp() { ToastUtil.showError(context, '考试时间已结束'); _submit(); } Widget _buildOptions(Question q) { if (q.questionType == '4') { return TextField( controller: TextEditingController(text: q.choiceAnswer), onChanged: (val) => q.choiceAnswer = val, maxLength: 255, decoration: const InputDecoration( hintText: '请输入内容', border: OutlineInputBorder(), ), ); } final keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D']; return Column( children: keys.map((key) { final active = q.choiceAnswer.split(',').contains(key); return GestureDetector( onTap: () => _chooseTopic( q.questionType == '3' ? 'judge' : (q.questionType == '2' ? 'multiple' : 'radio'), key, ), child: Container( margin: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 40, height: 40, alignment: Alignment.center, decoration: BoxDecoration( color: active ? Colors.blue : Colors.grey.shade200, shape: BoxShape.circle, ), child: Text( key, style: TextStyle( color: active ? Colors.white : Colors.black87, ), ), ), const SizedBox(width: 16), Expanded( child: Text( q.options[key] ?? '', style: const TextStyle(fontSize: 15), ), ), ], ), ), ); }).toList(), ); } String get _formattedTime { final m = remainingSeconds ~/ 60; final s = remainingSeconds % 60; return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { final q = questions.isNotEmpty ? questions[current] : null; return PopScope( canPop: true, // 禁用返回 child: Scaffold( backgroundColor: Colors.white, appBar: const MyAppbar(title: '课程考试', isBack: false), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ Container( width: double.infinity, height: 120, decoration: BoxDecoration( image: const DecorationImage( image: AssetImage('assets/study/bgimg1.png'), fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(8), ), ), Positioned.fill( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '试卷名称:${widget.examInfo['examName'] ?? ''}', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( '当前试题 ${current + 1}/${questions.length}', style: const TextStyle( color: Colors.white, fontSize: 15, ), ), const SizedBox(height: 8), Text( '考试剩余时间:$_formattedTime', style: const TextStyle( color: Colors.white, fontSize: 15, ), ), ], ), ), ), ], ), const SizedBox(height: 16), if (q != null) Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${current + 1}. ${q.questionDry} (${questionTypeMap[q.questionType] ?? ''})', style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 15, ), ), const SizedBox(height: 16), _buildOptions(q), const SizedBox(height: 24), ], ), ), ) else const Expanded(child: SizedBox()), const SizedBox(height: 8), Row( children: [ if (current > 0) Expanded( child: CustomButton( text: '上一题', backgroundColor: const Color(0xFFD7D7D7), textColor: Colors.black54, onPressed: _previousQuestion, ), ), if (current > 0 && current < questions.length - 1) const SizedBox(width: 16), if (current < questions.length - 1) Expanded( child: CustomButton( text: '下一题', backgroundColor: Colors.blue, onPressed: _nextQuestion, ), ), if (current == questions.length - 1) Expanded( child: CustomButton( text: '交卷', backgroundColor: Colors.blue, onPressed: _confirmSubmit, ), ), ], ), ], ), ), ), ); } }