/// ============================================================ /// 【QuestionModel】题目数据模型(纯 Dart 可变类) /// /// 设计说明(对应用户口型:纯 Dart 可变类、全量保留原字段名、 /// 手写 fromJson/toJson、不用 Isar/codegen): /// - 内部以 `Map` 承载全部字段(后端字段 + 解析器 /// 动态产出 + 运行时字段),字段名与 Vue 端完全一致; /// - 解析器(core/utils/question/)直接操作 Map,本类提供类型化 /// get/set 给页面与控制器使用; /// - childs/subQuestions 每次访问都基于同一批 Map 生成包装对象, /// 因此通过子模型的写操作会落到原始 Map 上(等价 JS 引用语义)。 /// ============================================================ library; class QuestionModel { QuestionModel(this.m); factory QuestionModel.fromJson(Map json) => QuestionModel(Map.from(json)); /// 原始数据(解析后的 Map,toJson 即提交回这个结构) final Map m; Map toJson() => m; @override String toString() => 'QuestionModel(id: $id, questionTypes: $questionTypes)'; // ---------------- 基础读写 ---------------- bool has(String key) => m.containsKey(key) && m[key] != null; T? get(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> 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 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? get answerAnalysisVideo { final v = m['answerAnalysisVideo']; return v is Map ? Map.from(v) : null; } List> get audios => mapList('audios'); List> 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 get childs => mapList('childs').map(QuestionModel.new).toList(); List get indexes => m['indexes'] is List ? m['indexes'] as List : const []; /// 笔记图片({id,url,thumbnailUrl} 等) List> get notes => mapList('notes'); set notes(List 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 get subQuestions => mapList('subQuestions').map(QuestionModel.new).toList(); List get bkTagMetaList => mapList('bkTagMetaList').map(QuestionModel.new).toList(); /// 选项 [{optionLabel, options}] List> get options { final v = m['options']; if (v is! List) return const []; return v.whereType().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? get raw { final v = m['raw']; return v is Map ? Map.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 option) => option['optionLabel'] ?? ''; String optionContentOf(Map option) => option['options'] ?? '';