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,171 @@
/// ============================================================
/// 题目数据适配层
///
/// 集中处理 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<Map<String, dynamic>> audios;
final List<Map<String, dynamic>> 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 = <Map<String, dynamic>>[];
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 = <Map<String, dynamic>>[];
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<Map<String, dynamic>> 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>().map((raw) {
final item = Map<String, dynamic>.from(raw);
item['url'] = item['Url'] ?? item['url'];
item['thumbnailUrl'] = item['ThumbnailUrl'] ?? item['thumbnailUrl'];
return item;
}).toList();
}
/// 安全解析:getSafeImageAnswerList(兜底解析 userAnswer / notes)
static List<Map<String, dynamic>> 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>()
.map((raw) {
final item = Map<String, dynamic>.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<UploadItem> toUploadItems(List<Map<String, dynamic>> 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<Map<String, dynamic>> uploadItemsToAnswerImgs(
List<UploadItem> 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<UploadItem> items) {
return jsonEncode(uploadItemsToAnswerImgs(items));
}
}