QinGang_interested/lib/pages/home/Study/study_take_exam_page.dart

459 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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<String, String> 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<String, dynamic> json) {
final type = '${json['questionType'] ?? ''}';
final opts = <String, String>{};
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<String, dynamic> 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<String, dynamic> examInfo;
final Map signInfo;
@override
State<StudyTakeExamPage> createState() => _StudyTakeExamPageState();
}
class _StudyTakeExamPageState extends State<StudyTakeExamPage> {
late final List<Question> questions;
late final Map<String, dynamic> info;
int current = 0;
late int remainingSeconds;
Timer? _timer;
final String _startExamTime = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
final questionTypeMap = <String, String>{
'1': '单选题',
'2': '多选题',
'3': '判断题',
'4': '填空题',
};
@override
void initState() {
super.initState();
info = widget.examInfo as Map<String, dynamic>? ?? {};
final rawList = widget.examInfo['questionList'] as List<dynamic>? ?? [];
questions = rawList.map((e) => Question.fromJson(e as Map<String, dynamic>)).toList();
final minutes = info['examTime'] as int? ?? 0;
remainingSeconds = minutes * 60;
_startTimer();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
Future<void> _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 ? <String>[] : 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<void> _submit() async {
LoadingDialogHelper.show(message: '正在提交');
// 按原逻辑:若多选题需要去掉逗号(保留你的业务要求)
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<Map<String,dynamic>>
};
final res = await EduApi.submitExam(data);
LoadingDialogHelper.hide();
if (res['success']) {
final data = res['data'] as Map<String, dynamic>? ?? {};
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,
),
),
],
),
],
),
),
),
);
}
}