172 lines
5.8 KiB
Dart
172 lines
5.8 KiB
Dart
/// ============================================================
|
||
/// 题目数据适配层
|
||
///
|
||
/// 集中处理 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));
|
||
}
|
||
}
|