/// ============================================================ /// 题目数据适配层 /// /// 集中处理 QuestionModel 与 UI 组件(RichTextContent / Upload) /// 之间的数据转换,对标 Vue classExercisesPage.vue 内的 /// stemRenderData / mergedAudios / mergedVideos / /// getImageAnswerList / getSafeImageAnswerList。 /// ============================================================ library; import 'dart:convert'; import 'package:new_learn/core/components/upload.dart'; import 'package:new_learn/core/utils/question/extract_media_from_stem.dart'; import 'package:new_learn/core/utils/question/question_parser.dart'; import 'question_model.dart'; /// 题干渲染数据(已格式化 + 已抽取并合并媒体) class StemRenderData { const StemRenderData({ required this.content, this.audios = const [], this.videos = const [], }); final String content; final List> audios; final List> videos; } class QuestionDataAdapter { QuestionDataAdapter._(); /// 等价 Vue 页内 stemRenderData + mergedAudios + mergedVideos static StemRenderData buildStemMedia(QuestionModel q, int activeSubIndex) { var stem = q.stem.isEmpty ? '暂无题干' : q.stem; final hasChilds = q.childs.isNotEmpty; final hasBkTags = q.bkTagMetaList.isNotEmpty; if (hasChilds || hasBkTags) { final disableHighlight = !hasChilds && q.bkTagMetaList.length == 1; final subQuestions = q.subQuestions.isNotEmpty ? q.subQuestions.map((e) => e.m).toList() : q.childs.map((e) => e.m).toList(); // 图片作答(1)不做填空锚点格式化 if (q.isFillblankItemUpload != 1) { stem = formatCompoundStem( stem, activeSubIndex, disableHighlight: disableHighlight, subQuestions: subQuestions, ); } // 数学学科:数字占位符转圈号 if ('${q.baseSubjectId}' == '2') { stem = convertBlankToCircled(stem); } } final media = extractMediaFromStem(stem); // 合并题干内嵌媒体与 q.audios/q.videos(按 url 去重,保持 JS 顺序) final audios = >[]; for (final item in [...media.audios, ...q.audios]) { final url = '${item['audioUrl'] ?? item['url'] ?? ''}'; if (url.isEmpty) continue; if (audios.any((it) => '${it['audioUrl'] ?? it['url']}' == url)) continue; audios.add(item); } final videos = >[]; for (final item in [...media.videos, ...q.videos]) { final url = '${item['videoUrl'] ?? item['url'] ?? ''}'; if (url.isEmpty) continue; if (videos.any((it) => '${it['videoUrl'] ?? it['url']}' == url)) continue; videos.add(item); } return StemRenderData( content: media.content.isNotEmpty ? media.content : (q.stem.isNotEmpty ? q.stem : '暂无题干'), audios: audios, videos: videos, ); } /// 单空填空题输入框值(对标 singleBlankInputValue) static String singleBlankInputValue(QuestionModel q) { final direct = q.userAnswer; if (direct.isNotEmpty) return direct; for (final child in q.childs) { final v = '${child.m['userAnswer'] ?? child.m['myAnswer'] ?? ''}'; if (v.isNotEmpty) return v; } return ''; } /// 严格解析:getImageAnswerList(外层保证是 JSON 数组字符串) static List> parseImageAnswerList(dynamic value) { if (value == null || '$value'.isEmpty) return const []; dynamic data; try { data = value is String ? jsonDecode(value) : value; } catch (_) { return const []; } if (data is! List) return const []; return data.whereType().map((raw) { final item = Map.from(raw); item['url'] = item['Url'] ?? item['url']; item['thumbnailUrl'] = item['ThumbnailUrl'] ?? item['thumbnailUrl']; return item; }).toList(); } /// 安全解析:getSafeImageAnswerList(兜底解析 userAnswer / notes) static List> safeImageAnswerList(dynamic value) { if (value == null || '$value'.isEmpty) return const []; dynamic list; try { list = value is String ? jsonDecode(value) : value; } catch (_) { return const []; } if (list is! List) return const []; return list .whereType() .map((raw) { final item = Map.from(raw); final url = '${item['url'] ?? item['Url'] ?? ''}'; final thumbnailUrl = '${item['thumbnailUrl'] ?? item['ThumbnailUrl'] ?? url}'; return {...item, 'url': url, 'thumbnailUrl': thumbnailUrl}; }) .where((item) => '${item['url']}'.isNotEmpty) .toList(); } /// 后端图片数据 → Upload 组件入参 static List toUploadItems(List> images) { return images .where((item) => '${item['url']}'.isNotEmpty) .map((item) => UploadItem( id: item['id'], url: '${item['url']}', thumbnailUrl: '${item['thumbnailUrl'] ?? item['url']}', )) .toList(); } /// Upload 组件当前列表 → 提交用 answerImgs /// (同时带 url/Url、thumbnailUrl/ThumbnailUrl 兼容后端) static List> uploadItemsToAnswerImgs( List items, ) { return items .where((e) => e.url.isNotEmpty) .map((e) => { 'url': e.url, 'Url': e.url, 'thumbnailUrl': e.thumbnailUrl ?? e.url, 'ThumbnailUrl': e.thumbnailUrl ?? e.url, }) .toList(); } /// Upload 组件当前列表 → 写回 userAnswer 的 JSON 字符串 /// (对标 Vue handleUploadChange: JSON.stringify(e)) static String uploadItemsToJson(List items) { return jsonEncode(uploadItemsToAnswerImgs(items)); } }