This commit is contained in:
chenshuainan
2026-09-28 20:12:34 +08:00
parent 847986f861
commit 5f53bd8f94
60 changed files with 14624 additions and 398 deletions
@@ -0,0 +1,267 @@
/// ============================================================
/// 【QuestionModel】题目数据模型(纯 Dart 可变类)
///
/// 设计说明(对应用户口型:纯 Dart 可变类、全量保留原字段名、
/// 手写 fromJson/toJson、不用 Isar/codegen):
/// - 内部以 `Map<String, dynamic>` 承载全部字段(后端字段 + 解析器
/// 动态产出 + 运行时字段),字段名与 Vue 端完全一致;
/// - 解析器(core/utils/question/)直接操作 Map,本类提供类型化
/// get/set 给页面与控制器使用;
/// - childs/subQuestions 每次访问都基于同一批 Map 生成包装对象,
/// 因此通过子模型的写操作会落到原始 Map 上(等价 JS 引用语义)。
/// ============================================================
library;
class QuestionModel {
QuestionModel(this.m);
factory QuestionModel.fromJson(Map<String, dynamic> json) =>
QuestionModel(Map<String, dynamic>.from(json));
/// 原始数据(解析后的 Map,toJson 即提交回这个结构)
final Map<String, dynamic> m;
Map<String, dynamic> toJson() => m;
@override
String toString() => 'QuestionModel(id: $id, questionTypes: $questionTypes)';
// ---------------- 基础读写 ----------------
bool has(String key) => m.containsKey(key) && m[key] != null;
T? get<T>(String key) => m[key] is T ? m[key] as T : null;
void set(String key, dynamic value) => m[key] = value;
String str(String key, [String def = '']) {
final v = m[key];
return v == null ? def : '$v';
}
int? intOf(String key) {
final v = m[key];
if (v is num) return v.toInt();
if (v == null) return null;
return int.tryParse('$v');
}
bool boolOf(String key, [bool def = false]) =>
m[key] == true ? true : (m[key] == false ? false : def);
/// 注意:返回的是原 Map 的 cast 视图(非拷贝),
/// 这样子模型上的所有写操作都会落回原始 JSON Map(等价 JS 引用语义)。
List<Map<String, dynamic>> mapList(String key) {
final v = m[key];
if (v is! List) return const [];
return [
for (final e in v)
if (e is Map) e.cast<String, dynamic>(),
];
}
// ---------------- 业务字段(原名) ----------------
String get id => str('id');
set id(dynamic v) => m['id'] = v;
String get questionId => str('questionId');
set questionId(dynamic v) => m['questionId'] = v;
dynamic get questionTypeId => m['questionTypeId'];
set questionTypeId(dynamic v) => m['questionTypeId'] = v;
String get questionTypeName => str('questionTypeName');
set questionTypeName(String v) => m['questionTypeName'] = v;
dynamic get questionTypeCode => m['questionTypeCode'];
bool get isObjective => boolOf('isObjective');
bool get isSelect => boolOf('isSelect');
String get stem => str('stem');
set stem(String v) => m['stem'] = v;
String get answer => str('answer');
set answer(String v) => m['answer'] = v;
String get correctAnswer => str('correctAnswer');
set correctAnswer(String v) => m['correctAnswer'] = v;
String get userAnswer => str('userAnswer');
set userAnswer(dynamic v) => m['userAnswer'] = v;
/// 部分接口字段(与 userAnswer 互为兜底)
String get myAnswer => str('myAnswer');
set myAnswer(dynamic v) => m['myAnswer'] = v;
/// Vue 端 child.userAnswer || child.myAnswer
String get effectiveAnswer {
final u = m['userAnswer'];
return u != null && '$u'.isNotEmpty ? '$u' : myAnswer;
}
int? get answerStatus => intOf('answerStatus');
set answerStatus(dynamic v) => m['answerStatus'] = v;
/// 解析(字符串或 {analysis: '...'})
dynamic get answerAnalysisRaw => m['answerAnalysis'];
set answerAnalysisRaw(dynamic v) => m['answerAnalysis'] = v;
String get answerAnalysisText {
final v = m['answerAnalysis'];
if (v is Map) return '${v['analysis'] ?? ''}';
return v == null ? '' : '$v';
}
Map<String, dynamic>? get answerAnalysisVideo {
final v = m['answerAnalysisVideo'];
return v is Map ? Map<String, dynamic>.from(v) : null;
}
List<Map<String, dynamic>> get audios => mapList('audios');
List<Map<String, dynamic>> get videos => mapList('videos');
int get planTimeSecond => intOf('planTimeSecond') ?? 0;
set planTimeSecond(dynamic v) => m['planTimeSecond'] = v;
/// 运行时:本地剩余秒缓存
int? get newPlanTimeSecond => intOf('NewPlanTimeSecond');
set newPlanTimeSecond(int? v) => m['NewPlanTimeSecond'] = v;
/// 运行时:本题建议时长是否已完成
bool get isFinishPlanTime => boolOf('ISFinishPlanTime');
set isFinishPlanTime(bool v) => m['ISFinishPlanTime'] = v;
int get answerTimeSecond => intOf('answerTimeSecond') ?? 0;
dynamic get score => m['score'];
dynamic get userScore => m['userScore'];
bool get isCollect => boolOf('isCollect');
set isCollect(bool v) => m['isCollect'] = v;
dynamic get collectId => m['collectId'];
set collectId(dynamic v) => m['collectId'] = v;
bool get isWrongCollect => boolOf('isWrongCollect');
set isWrongCollect(bool v) => m['isWrongCollect'] = v;
dynamic get wrongCollectId => m['wrongCollectId'];
set wrongCollectId(dynamic v) => m['wrongCollectId'] = v;
bool get hasChild => boolOf('hasChild');
List<QuestionModel> get childs =>
mapList('childs').map(QuestionModel.new).toList();
List<dynamic> get indexes =>
m['indexes'] is List ? m['indexes'] as List : const [];
/// 笔记图片({id,url,thumbnailUrl} 等)
List<Map<String, dynamic>> get notes => mapList('notes');
set notes(List<dynamic> v) => m['notes'] = v;
String get source => str('source');
String? get wrongReason =>
m['wrongReason'] == null ? null : '${m['wrongReason']}';
String? get wrongAdvice =>
m['wrongAdvice'] == null ? null : '${m['wrongAdvice']}';
bool get isUploadNotes => boolOf('isUploadNotes');
/// 加载时注入:0 输入框 / 1 图片作答
int get isFillblankItemUpload => intOf('iSFilllankItemUpload') ?? 0;
set isFillblankItemUpload(dynamic v) => m['iSFilllankItemUpload'] = v;
dynamic get baseSubjectId => m['baseSubjectId'];
set baseSubjectId(dynamic v) => m['baseSubjectId'] = v;
int? get sort => intOf('sort');
set sort(dynamic v) => m['sort'] = v;
int? get wrongSort => intOf('wrongSort');
set wrongSort(dynamic v) => m['wrongSort'] = v;
/// appendExamQuestionChildOptionStatus 注入
bool get hasOptionChildrenQuestion => boolOf('hasOptionChildrenQuestion');
set hasOptionChildrenQuestion(bool v) =>
m['hasOptionChildrenQuestion'] = v;
List<QuestionModel> get subQuestions =>
mapList('subQuestions').map(QuestionModel.new).toList();
List<QuestionModel> get bkTagMetaList =>
mapList('bkTagMetaList').map(QuestionModel.new).toList();
/// 选项 [{optionLabel, options}]
List<Map<String, String>> get options {
final v = m['options'];
if (v is! List) return const [];
return v.whereType<Map>().map((e) {
return {
'optionLabel': '${e['optionLabel'] ?? ''}',
'options': '${e['options'] ?? ''}',
};
}).toList();
}
String get questionTypes => str('questionTypes');
set questionTypes(String v) => m['questionTypes'] = v;
String get analysis => str('analysis');
set analysis(String v) => m['analysis'] = v;
/// 解析时保留的原始对象
Map<String, dynamic>? get raw {
final v = m['raw'];
return v is Map ? Map<String, dynamic>.from(v) : null;
}
/// 运行时:进入本题时间(本地 ISO)
String? get startTime => m['startTime'] as String?;
set startTime(String? v) => m['startTime'] = v;
/// 运行时:离开本题时间(本地 ISO)
String? get endTime => m['endTime'] as String?;
set endTime(String? v) => m['endTime'] = v;
/// 子题号(如 "1.1",subQuestions 元素自带)
dynamic get index => m['index'];
/// 运行时:列表下标
int? get currentIndex => intOf('currentIndex');
set currentIndex(int v) => m['currentIndex'] = v;
// ---------------- 便捷方法 ----------------
QuestionModel? childAt(int index) {
final list = mapList('childs');
if (index < 0 || index >= list.length) return null;
return QuestionModel(list[index]);
}
QuestionModel? subAt(int index) {
final list = mapList('subQuestions');
if (index < 0 || index >= list.length) return null;
return QuestionModel(list[index]);
}
/// 重置本题及其子题的 startTime(对应 Vue resetCurrentQuestionStartTime)
void resetStartTime(String isoNow) {
m['startTime'] = isoNow;
for (final key in ['subQuestions', 'childs']) {
final v = m[key];
if (v is List) {
for (final c in v) {
if (c is Map) c['startTime'] = isoNow;
}
}
}
}
}
/// 选项取值辅助
String optionLabelOf(Map<String, String> option) => option['optionLabel'] ?? '';
String optionContentOf(Map<String, String> option) => option['options'] ?? '';