flutter_integrated_whb/lib/pages/home/study/take_exam_page.dart

405 lines
12 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.

import 'dart:async';
import 'package:flutter/material.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/pages/home/study/study_detail_page.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'dart:convert';
class Question {
final Map<String, dynamic> rawData;
final String questionDry;
final String questionType;
final Map<String, String> options;
String checked;
Question({
required this.rawData,
required this.questionDry,
required this.questionType,
required this.options,
this.checked = '',
});
factory Question.fromJson(Map<String, dynamic> json) {
final type = json['QUESTIONTYPE'] as String? ?? '1';
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 raw = Map<String, dynamic>.from(json);
return Question(
rawData: raw,
questionDry: json['QUESTIONDRY'] as String? ?? '',
questionType: type,
options: opts,
);
}
}
class TakeExamPage extends StatefulWidget {
const TakeExamPage({
required this.examInfo,
required this.examType,
super.key,
});
final Map<String, dynamic> examInfo;
final TakeExamType examType;
@override
State<TakeExamPage> createState() => _TakeExamPageState();
}
class _TakeExamPageState extends State<TakeExamPage> {
late final List<Question> questions;
late final Map<String, dynamic> info;
int current = 0;
late int remainingSeconds;
Timer? _timer;
final questionTypeMap = <String, String>{
'1': '单选题',
'2': '多选题',
'3': '判断题',
'4': '填空题',
};
@override
void initState() {
super.initState();
info = widget.examInfo['pd'] as Map<String, dynamic>? ?? {};
final rawList = widget.examInfo['inputQue'] as List<dynamic>? ?? [];
questions =
rawList
.map((e) => Question.fromJson(e as Map<String, dynamic>))
.toList();
final numberOfExams = widget.examInfo['NUMBEROFEXAMS'] as String? ?? '0';
WidgetsBinding.instance.addPostFrameCallback((_) {
if (numberOfExams == '-9999') {
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
} else {
_showTip('您无考试次数!');
}
});
final minutes = info['ANSWERSHEETTIME'] as int? ?? 0;
remainingSeconds = minutes * 60;
_startTimer();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
Future<void> _showTip(String content) {
return showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content: content,
cancelText: '',
confirmText: '确定',
onConfirm: () {},
),
);
}
bool _validateCurrentAnswer() {
final q = questions[current];
if (q.checked.isEmpty) {
_showTip('请对本题进行作答。');
return false;
}
if (q.questionType == '2' && q.checked.split(',').length < 2) {
_showTip('多选题最少需要选择两个答案。');
return false;
}
return true;
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingSeconds <= 0) {
timer.cancel();
_onTimeUp();
} else {
setState(() => remainingSeconds--);
}
});
}
void _chooseTopic(String type, String key) {
final q = questions[current];
if (type == 'radio' || type == 'judge') {
q.checked = (q.checked == key) ? '' : key;
} else {
final arr = q.checked.isNotEmpty ? q.checked.split(',') : <String>[];
if (arr.contains(key))
arr.remove(key);
else
arr.add(key);
arr.sort();
q.checked = arr.join(',');
}
setState(() {});
}
void _nextQuestion() {
if (!_validateCurrentAnswer()) return;
if (current < questions.length - 1) setState(() => current++);
}
void _previousQuestion() {
if (current > 0) setState(() => current--);
}
void _confirmSubmit() {
if (!_validateCurrentAnswer()) return;
showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content: '请确认是否交卷!',
cancelText: '取消',
onCancel: () {},
onConfirm: _submit,
),
);
}
Future<void> _submit() async {
for (var q in questions) {
if (q.questionType == '2') q.checked = q.checked.replaceAll(',', '');
q.rawData['checked'] = q.checked;
}
final data = {
'STAGEEXAMPAPERINPUT_ID':
widget.examInfo['STRENGTHEN_PAPER_QUESTION_ID'],
'STUDENT_ID': widget.examInfo['STUDENT_ID'],
'CLASS_ID': widget.examInfo['CLASS_ID'],
'NUMBEROFEXAMS': widget.examInfo['NUMBEROFEXAMS'],
'entrySite': widget.examType.name,
'PASSSCORE': info['PASSSCORE'],
'EXAMSCORE': info['EXAMSCORE'],
'EXAMTIMEBEGIN': info['EXAMTIMEBEGIN'],
'options': jsonEncode(questions.map((q) => q.rawData).toList()),
};
final res = await ApiService.submitExam(data);
if (res['result'] == 'success') {
final score = res['examScore'] ?? '0';
final passed = res['examResult'] != '0';
showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content:
passed
? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!'
: '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!',
cancelText: '',
confirmText: '确定',
onConfirm: () {
Navigator.pop(context);
},
),
);
}
}
void _onTimeUp() {
ToastUtil.showError(context, '考试时间已结束');
_submit();
}
Widget _buildOptions(Question q) {
if (q.questionType == '4') {
return TextField(
controller: TextEditingController(text: q.checked),
onChanged: (val) => q.checked = 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.checked.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: 16),
),
),
],
),
),
);
}).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: false, // 禁用返回
child: Scaffold(
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(
'考试科目:${info['EXAMNAME'] ?? ''}',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'当前试题 ${current + 1}/${questions.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
'考试剩余时间:$_formattedTime',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
if (q != null) ...[
Text(
'${current + 1}. ${q.questionDry} ${questionTypeMap[q.questionType] ?? ''}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 16),
_buildOptions(q),
],
const Spacer(),
Row(
children: [
if (current > 0)
Expanded(
child: CustomButton(
text: '上一题',
backgroundColor: Colors.grey.shade200,
textStyle: const TextStyle(color: 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,
),
),
],
),
],
),
),
),
);
}
}