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

405 lines
12 KiB
Dart
Raw Normal View History

2025-07-22 13:34:34 +08:00
import 'dart:async';
2025-07-18 17:13:38 +08:00
import 'package:flutter/material.dart';
2025-07-22 13:34:34 +08:00
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';
2025-07-18 17:13:38 +08:00
import 'package:qhd_prevention/pages/my_appbar.dart';
2025-07-22 13:34:34 +08:00
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,
);
}
}
2025-07-18 17:13:38 +08:00
class TakeExamPage extends StatefulWidget {
2025-07-22 13:34:34 +08:00
const TakeExamPage({
required this.examInfo,
required this.examType,
super.key,
});
final Map<String, dynamic> examInfo;
final TakeExamType examType;
2025-07-18 17:13:38 +08:00
@override
State<TakeExamPage> createState() => _TakeExamPageState();
}
class _TakeExamPageState extends State<TakeExamPage> {
2025-07-22 13:34:34 +08:00
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')}';
}
2025-07-18 17:13:38 +08:00
@override
Widget build(BuildContext context) {
2025-07-22 13:34:34 +08:00
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,
),
),
],
),
],
),
),
),
2025-07-18 17:13:38 +08:00
);
}
}