更新
This commit is contained in:
@@ -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'] ?? '';
|
||||
Reference in New Issue
Block a user