268 lines
8.8 KiB
Dart
268 lines
8.8 KiB
Dart
/// ============================================================
|
||
/// 【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'] ?? '';
|