更新
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user