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

461 lines
15 KiB
Dart
Raw Normal View History

2026-02-28 14:38:07 +08:00
// 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 {
// 按原逻辑:若多选题需要去掉逗号(保留你的业务要求)
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>>
};
2026-03-05 16:12:47 +08:00
// final r = await CustomAlertDialog.showConfirm(context, title: '参数', content: jsonEncode(data));
// if (!r) return;
LoadingDialogHelper.show(message: '正在提交');
2026-02-28 14:38:07 +08:00
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,
),
),
],
),
],
),
),
),
);
}
}