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
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|