245 lines
8.5 KiB
Dart
245 lines
8.5 KiB
Dart
/// ============================================================
|
||
/// 【ObjectiveAnswerView】客观题答案对比卡片
|
||
/// (1:1 迁移 Vue components/AnswerView.vue 的 type == 'objective' 分支;
|
||
/// 本页只使用客观题形态)
|
||
///
|
||
/// 视觉:
|
||
/// 218rpx 卡片:上行作答状态图标+文案(10 正确 / 11 错误 / 12 部分正确 /
|
||
/// 0 未作答),下行白色对比块:"正确答案:X / 你的答案:Y"
|
||
/// ============================================================
|
||
library;
|
||
|
||
import 'package:cached_network_image/cached_network_image.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
import 'package:new_learn/core/components/rich_text_content.dart';
|
||
import 'package:new_learn/core/utils/img_url.dart';
|
||
import 'package:new_learn/core/utils/question/question_parser.dart';
|
||
|
||
import 'exercise_styles.dart';
|
||
|
||
class ObjectiveAnswerView extends StatelessWidget {
|
||
const ObjectiveAnswerView({
|
||
super.key,
|
||
required this.answerStatus,
|
||
this.userAnswer = '',
|
||
this.correctAnswer = '',
|
||
});
|
||
|
||
/// 0 未作答 / 10 正确 / 11 错误 / 12 部分正确
|
||
final dynamic answerStatus;
|
||
final String userAnswer;
|
||
final String correctAnswer;
|
||
|
||
int get _status => int.tryParse('$answerStatus') ?? 0;
|
||
|
||
bool get _isCorrect => _status == 10;
|
||
|
||
/// 客观题文字展示清洗(对标 formatOptionText),仅用于判空与纯文本展示
|
||
String _formatOptionText(dynamic ans) {
|
||
if (ans == null) return '';
|
||
|
||
String str;
|
||
if (ans is List) {
|
||
str = ans.join();
|
||
} else if (ans is Map) {
|
||
str = ans.toString();
|
||
if (str == '{}' || str == '[]') return '';
|
||
} else {
|
||
str = '$ans';
|
||
}
|
||
|
||
final trimmed = str.trim();
|
||
if (trimmed.isEmpty || trimmed == '[]' || trimmed == '{}' ||
|
||
trimmed == '""' || trimmed == "''") {
|
||
return '';
|
||
}
|
||
|
||
var result = trimmed.replaceAll(RegExp(r'<[^>]+>'), '');
|
||
result = result.replaceAll(' ', ' ');
|
||
result = result.replaceAll(RegExp(r'&[a-z]+;'), '');
|
||
result = result.replaceAll(RegExp(r'\s+'), ' ');
|
||
result = result.replaceAll(RegExp(r'^(\d+[\.\s、)\s]+|第\d+题)'), '');
|
||
result = result.replaceAll(',', '');
|
||
return result.trim();
|
||
}
|
||
|
||
/// 清洗后的答案 HTML(富文本渲染用)
|
||
String _rendered(String html) => cleanRichText(html);
|
||
|
||
bool _hasFormula(String html) =>
|
||
RegExp(r'math-formula|katex').hasMatch(html);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final correctText = _formatOptionText(correctAnswer);
|
||
final userText = _formatOptionText(userAnswer);
|
||
final correctDisplay = correctText.isEmpty ? '无' : correctText;
|
||
final userDisplay = userText.isEmpty ? '未作答' : userText;
|
||
|
||
final statusText = _status == 10
|
||
? '回答正确'
|
||
: _status == 11
|
||
? '回答错误'
|
||
: _status == 12
|
||
? '部分正确'
|
||
: '未作答';
|
||
|
||
final accent = _isCorrect ? ExerciseColors.correct : ExerciseColors.error;
|
||
|
||
final correctHtml = _rendered(correctAnswer);
|
||
final userHtml = _rendered(userAnswer);
|
||
|
||
return Container(
|
||
height: 109.h, // 218rpx
|
||
decoration: BoxDecoration(
|
||
// CSS 外层:实色 padding-box + 180deg 渐变 border-box
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: _isCorrect
|
||
? const [Color(0x8000CC96), Color(0x1A00CC96), Color(0x8000CC96)]
|
||
: const [Color(0x80FF4C4C), Color(0x1AFF4C4C), Color(0x80FF4C4C)],
|
||
),
|
||
borderRadius: BorderRadius.circular(12.r),
|
||
),
|
||
child: Container(
|
||
margin: EdgeInsets.all(1.w),
|
||
decoration: BoxDecoration(
|
||
color: _isCorrect ? const Color(0xFFEBFFFA) : const Color(0xFFFFF0F0),
|
||
borderRadius: BorderRadius.circular(11.r),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 第一行:状态提示
|
||
Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
|
||
child: Row(
|
||
children: [
|
||
CachedNetworkImage(
|
||
imageUrl: ImgUrl.build(
|
||
_isCorrect
|
||
? '/StaticResource/2026-06-01/correct-icon1780284329572.png'
|
||
: '/StaticResource/2026-06-01/error-icon1780284292405.png',
|
||
),
|
||
width: 16.w,
|
||
height: 16.w,
|
||
),
|
||
SizedBox(width: 6.w),
|
||
Text(
|
||
statusText,
|
||
style: TextStyle(
|
||
fontSize: 14.sp,
|
||
fontWeight: FontWeight.w600,
|
||
color: accent,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 第二行:答案对比
|
||
Expanded(
|
||
child: Container(
|
||
margin: EdgeInsets.fromLTRB(1.w, 0, 1.w, 1.w),
|
||
padding: EdgeInsets.fromLTRB(14.w, 0, 26.w, 0),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(10.r),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: _CompareItem(
|
||
label: '正确答案:',
|
||
child: _hasFormula(correctHtml)
|
||
? RichTextContent(
|
||
html: correctHtml,
|
||
textStyle: TextStyle(
|
||
fontSize: 14.sp,
|
||
color: const Color(0xFF333333),
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
)
|
||
: Text(
|
||
correctDisplay,
|
||
style: TextStyle(
|
||
fontSize: 14.sp,
|
||
color: const Color(0xFF333333),
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: _CompareItem(
|
||
alignEnd: true,
|
||
label: '你的答案:',
|
||
child: _hasFormula(userHtml)
|
||
? RichTextContent(
|
||
html: userHtml,
|
||
textStyle: TextStyle(
|
||
fontSize: 14.sp,
|
||
color: _isCorrect
|
||
? ExerciseColors.correct
|
||
: (userText.isEmpty
|
||
? const Color(0xFF999999)
|
||
: ExerciseColors.error),
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
)
|
||
: Text(
|
||
userDisplay,
|
||
style: TextStyle(
|
||
fontSize: 14.sp,
|
||
color: _isCorrect
|
||
? ExerciseColors.correct
|
||
: (userText.isEmpty
|
||
? const Color(0xFF999999)
|
||
: ExerciseColors.error),
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CompareItem extends StatelessWidget {
|
||
const _CompareItem({
|
||
required this.label,
|
||
required this.child,
|
||
this.alignEnd = false,
|
||
});
|
||
|
||
final String label;
|
||
final Widget child;
|
||
final bool alignEnd;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final row = Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Flexible(
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(fontSize: 14.sp, color: ExerciseColors.subtitle),
|
||
),
|
||
),
|
||
Flexible(child: child),
|
||
],
|
||
);
|
||
return alignEnd
|
||
? Align(alignment: Alignment.centerRight, child: row)
|
||
: Align(alignment: Alignment.centerLeft, child: row);
|
||
}
|
||
}
|