405 lines
12 KiB
Dart
405 lines
12 KiB
Dart
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|