Commit fd737d6ae275311fe88db3416e3dd53cbd382d9d
1 parent
d0623cfd
feat:绘本作答结果以单词为单位根据分数展示不同颜色
Showing
7 changed files
with
261 additions
and
9 deletions
lib/generated/json/base/json_convert_content.dart
... | ... | @@ -16,6 +16,7 @@ import 'package:wow_english/models/follow_read_entity.dart'; |
16 | 16 | import 'package:wow_english/models/listen_entity.dart'; |
17 | 17 | import 'package:wow_english/models/product_entity.dart'; |
18 | 18 | import 'package:wow_english/models/read_content_entity.dart'; |
19 | +import 'package:wow_english/models/singsound_result_detail_entity.dart'; | |
19 | 20 | import 'package:wow_english/models/user_entity.dart'; |
20 | 21 | |
21 | 22 | JsonConvert jsonConvert = JsonConvert(); |
... | ... | @@ -232,6 +233,10 @@ class JsonConvert { |
232 | 233 | return data.map<ReadContentEntity>((Map<String, dynamic> e) => |
233 | 234 | ReadContentEntity.fromJson(e)).toList() as M; |
234 | 235 | } |
236 | + if (<SingsoundResultDetailEntity>[] is M) { | |
237 | + return data.map<SingsoundResultDetailEntity>((Map<String, dynamic> e) => | |
238 | + SingsoundResultDetailEntity.fromJson(e)).toList() as M; | |
239 | + } | |
235 | 240 | if (<UserEntity>[] is M) { |
236 | 241 | return data.map<UserEntity>((Map<String, dynamic> e) => |
237 | 242 | UserEntity.fromJson(e)).toList() as M; |
... | ... | @@ -278,6 +283,8 @@ class JsonConvertClassCollection { |
278 | 283 | (ListenEntity).toString(): ListenEntity.fromJson, |
279 | 284 | (ProductEntity).toString(): ProductEntity.fromJson, |
280 | 285 | (ReadContentEntity).toString(): ReadContentEntity.fromJson, |
286 | + (SingsoundResultDetailEntity).toString(): SingsoundResultDetailEntity | |
287 | + .fromJson, | |
281 | 288 | (UserEntity).toString(): UserEntity.fromJson, |
282 | 289 | }; |
283 | 290 | ... | ... |
lib/generated/json/course_process_entity.g.dart
1 | 1 | import 'package:wow_english/generated/json/base/json_convert_content.dart'; |
2 | 2 | import 'package:wow_english/models/course_process_entity.dart'; |
3 | +import 'package:wow_english/models/singsound_result_detail_entity.dart'; | |
4 | + | |
3 | 5 | |
4 | 6 | CourseProcessEntity $CourseProcessEntityFromJson(Map<String, dynamic> json) { |
5 | 7 | final CourseProcessEntity courseProcessEntity = CourseProcessEntity(); |
... | ... | @@ -114,6 +116,15 @@ CourseProcessReadings $CourseProcessReadingsFromJson( |
114 | 116 | if (recordScore != null) { |
115 | 117 | courseProcessReadings.recordScore = recordScore; |
116 | 118 | } |
119 | + final List< | |
120 | + SingsoundResultDetailEntity>? resultDetails = (json['resultDetails'] as List< | |
121 | + dynamic>?)?.map( | |
122 | + (e) => | |
123 | + jsonConvert.convert<SingsoundResultDetailEntity>( | |
124 | + e) as SingsoundResultDetailEntity).toList(); | |
125 | + if (resultDetails != null) { | |
126 | + courseProcessReadings.resultDetails = resultDetails; | |
127 | + } | |
117 | 128 | return courseProcessReadings; |
118 | 129 | } |
119 | 130 | |
... | ... | @@ -132,6 +143,7 @@ Map<String, dynamic> $CourseProcessReadingsToJson( |
132 | 143 | data['word'] = entity.word; |
133 | 144 | data['recordUrl'] = entity.recordUrl; |
134 | 145 | data['recordScore'] = entity.recordScore; |
146 | + data['resultDetails'] = entity.resultDetails?.map((v) => v.toJson()).toList(); | |
135 | 147 | return data; |
136 | 148 | } |
137 | 149 | |
... | ... | @@ -149,6 +161,7 @@ extension CourseProcessReadingsExtension on CourseProcessReadings { |
149 | 161 | String? word, |
150 | 162 | String? recordUrl, |
151 | 163 | String? recordScore, |
164 | + List<SingsoundResultDetailEntity>? resultDetails, | |
152 | 165 | }) { |
153 | 166 | return CourseProcessReadings() |
154 | 167 | ..audioUrl = audioUrl ?? this.audioUrl |
... | ... | @@ -162,7 +175,8 @@ extension CourseProcessReadingsExtension on CourseProcessReadings { |
162 | 175 | ..sortOrder = sortOrder ?? this.sortOrder |
163 | 176 | ..word = word ?? this.word |
164 | 177 | ..recordUrl = recordUrl ?? this.recordUrl |
165 | - ..recordScore = recordScore ?? this.recordScore; | |
178 | + ..recordScore = recordScore ?? this.recordScore | |
179 | + ..resultDetails = resultDetails ?? this.resultDetails; | |
166 | 180 | } |
167 | 181 | } |
168 | 182 | ... | ... |
lib/generated/json/singsound_result_detail_entity.g.dart
0 → 100644
1 | +import 'package:wow_english/generated/json/base/json_convert_content.dart'; | |
2 | +import 'package:wow_english/models/singsound_result_detail_entity.dart'; | |
3 | + | |
4 | +SingsoundResultDetailEntity $SingsoundResultDetailEntityFromJson( | |
5 | + Map<String, dynamic> json) { | |
6 | + final SingsoundResultDetailEntity singsoundResultDetailEntity = SingsoundResultDetailEntity(); | |
7 | + final int? dpType = jsonConvert.convert<int>(json['dp_type']); | |
8 | + if (dpType != null) { | |
9 | + singsoundResultDetailEntity.dpType = dpType; | |
10 | + } | |
11 | + final int? tonescore = jsonConvert.convert<int>(json['tonescore']); | |
12 | + if (tonescore != null) { | |
13 | + singsoundResultDetailEntity.tonescore = tonescore; | |
14 | + } | |
15 | + final int? dur = jsonConvert.convert<int>(json['dur']); | |
16 | + if (dur != null) { | |
17 | + singsoundResultDetailEntity.dur = dur; | |
18 | + } | |
19 | + final int? liaisonref = jsonConvert.convert<int>(json['liaisonref']); | |
20 | + if (liaisonref != null) { | |
21 | + singsoundResultDetailEntity.liaisonref = liaisonref; | |
22 | + } | |
23 | + final int? stressref = jsonConvert.convert<int>(json['stressref']); | |
24 | + if (stressref != null) { | |
25 | + singsoundResultDetailEntity.stressref = stressref; | |
26 | + } | |
27 | + final int? senseref = jsonConvert.convert<int>(json['senseref']); | |
28 | + if (senseref != null) { | |
29 | + singsoundResultDetailEntity.senseref = senseref; | |
30 | + } | |
31 | + final int? start = jsonConvert.convert<int>(json['start']); | |
32 | + if (start != null) { | |
33 | + singsoundResultDetailEntity.start = start; | |
34 | + } | |
35 | + final int? liaisonscore = jsonConvert.convert<int>(json['liaisonscore']); | |
36 | + if (liaisonscore != null) { | |
37 | + singsoundResultDetailEntity.liaisonscore = liaisonscore; | |
38 | + } | |
39 | + final int? fluency = jsonConvert.convert<int>(json['fluency']); | |
40 | + if (fluency != null) { | |
41 | + singsoundResultDetailEntity.fluency = fluency; | |
42 | + } | |
43 | + final String? char = jsonConvert.convert<String>(json['char']); | |
44 | + if (char != null) { | |
45 | + singsoundResultDetailEntity.char = char; | |
46 | + } | |
47 | + final int? toneref = jsonConvert.convert<int>(json['toneref']); | |
48 | + if (toneref != null) { | |
49 | + singsoundResultDetailEntity.toneref = toneref; | |
50 | + } | |
51 | + final int? stressscore = jsonConvert.convert<int>(json['stressscore']); | |
52 | + if (stressscore != null) { | |
53 | + singsoundResultDetailEntity.stressscore = stressscore; | |
54 | + } | |
55 | + final int? score = jsonConvert.convert<int>(json['score']); | |
56 | + if (score != null) { | |
57 | + singsoundResultDetailEntity.score = score; | |
58 | + } | |
59 | + final int? end = jsonConvert.convert<int>(json['end']); | |
60 | + if (end != null) { | |
61 | + singsoundResultDetailEntity.end = end; | |
62 | + } | |
63 | + final int? sensescore = jsonConvert.convert<int>(json['sensescore']); | |
64 | + if (sensescore != null) { | |
65 | + singsoundResultDetailEntity.sensescore = sensescore; | |
66 | + } | |
67 | + return singsoundResultDetailEntity; | |
68 | +} | |
69 | + | |
70 | +Map<String, dynamic> $SingsoundResultDetailEntityToJson( | |
71 | + SingsoundResultDetailEntity entity) { | |
72 | + final Map<String, dynamic> data = <String, dynamic>{}; | |
73 | + data['dp_type'] = entity.dpType; | |
74 | + data['tonescore'] = entity.tonescore; | |
75 | + data['dur'] = entity.dur; | |
76 | + data['liaisonref'] = entity.liaisonref; | |
77 | + data['stressref'] = entity.stressref; | |
78 | + data['senseref'] = entity.senseref; | |
79 | + data['start'] = entity.start; | |
80 | + data['liaisonscore'] = entity.liaisonscore; | |
81 | + data['fluency'] = entity.fluency; | |
82 | + data['char'] = entity.char; | |
83 | + data['toneref'] = entity.toneref; | |
84 | + data['stressscore'] = entity.stressscore; | |
85 | + data['score'] = entity.score; | |
86 | + data['end'] = entity.end; | |
87 | + data['sensescore'] = entity.sensescore; | |
88 | + return data; | |
89 | +} | |
90 | + | |
91 | +extension SingsoundResultDetailEntityExtension on SingsoundResultDetailEntity { | |
92 | + SingsoundResultDetailEntity copyWith({ | |
93 | + int? dpType, | |
94 | + int? tonescore, | |
95 | + int? dur, | |
96 | + int? liaisonref, | |
97 | + int? stressref, | |
98 | + int? senseref, | |
99 | + int? start, | |
100 | + int? liaisonscore, | |
101 | + int? fluency, | |
102 | + String? char, | |
103 | + int? toneref, | |
104 | + int? stressscore, | |
105 | + int? score, | |
106 | + int? end, | |
107 | + int? sensescore, | |
108 | + }) { | |
109 | + return SingsoundResultDetailEntity() | |
110 | + ..dpType = dpType ?? this.dpType | |
111 | + ..tonescore = tonescore ?? this.tonescore | |
112 | + ..dur = dur ?? this.dur | |
113 | + ..liaisonref = liaisonref ?? this.liaisonref | |
114 | + ..stressref = stressref ?? this.stressref | |
115 | + ..senseref = senseref ?? this.senseref | |
116 | + ..start = start ?? this.start | |
117 | + ..liaisonscore = liaisonscore ?? this.liaisonscore | |
118 | + ..fluency = fluency ?? this.fluency | |
119 | + ..char = char ?? this.char | |
120 | + ..toneref = toneref ?? this.toneref | |
121 | + ..stressscore = stressscore ?? this.stressscore | |
122 | + ..score = score ?? this.score | |
123 | + ..end = end ?? this.end | |
124 | + ..sensescore = sensescore ?? this.sensescore; | |
125 | + } | |
126 | +} | |
0 | 127 | \ No newline at end of file | ... | ... |
lib/models/course_process_entity.dart
... | ... | @@ -2,6 +2,8 @@ import 'package:wow_english/generated/json/base/json_field.dart'; |
2 | 2 | import 'package:wow_english/generated/json/course_process_entity.g.dart'; |
3 | 3 | import 'dart:convert'; |
4 | 4 | |
5 | +import 'package:wow_english/models/singsound_result_detail_entity.dart'; | |
6 | + | |
5 | 7 | @JsonSerializable() |
6 | 8 | class CourseProcessEntity { |
7 | 9 | int? currentStep; |
... | ... | @@ -36,6 +38,7 @@ class CourseProcessReadings { |
36 | 38 | String? word; |
37 | 39 | String? recordUrl; |
38 | 40 | String? recordScore; |
41 | + List<SingsoundResultDetailEntity>? resultDetails; | |
39 | 42 | |
40 | 43 | CourseProcessReadings(); |
41 | 44 | ... | ... |
lib/models/singsound_result_detail_entity.dart
0 → 100644
1 | +import 'package:wow_english/generated/json/base/json_field.dart'; | |
2 | +import 'package:wow_english/generated/json/singsound_result_detail_entity.g.dart'; | |
3 | +import 'dart:convert'; | |
4 | +export 'package:wow_english/generated/json/singsound_result_detail_entity.g.dart'; | |
5 | + | |
6 | +@JsonSerializable() | |
7 | +class SingsoundResultDetailEntity { | |
8 | + @JSONField(name: "dp_type") | |
9 | + late int? dpType; | |
10 | + late int? tonescore; | |
11 | + late int? dur; | |
12 | + late int? liaisonref; | |
13 | + late int? stressref; | |
14 | + late int? senseref; | |
15 | + late int? start; | |
16 | + late int? liaisonscore; | |
17 | + late int? fluency; | |
18 | + late String char; | |
19 | + late int? toneref; | |
20 | + late int? stressscore; | |
21 | + late int score; | |
22 | + late int? end; | |
23 | + late int? sensescore; | |
24 | + | |
25 | + SingsoundResultDetailEntity(); | |
26 | + | |
27 | + // 只接受两个参数的构造函数 | |
28 | + SingsoundResultDetailEntity.withCharAndScore(this.char, this.score) { | |
29 | + dpType = null; | |
30 | + tonescore = null; | |
31 | + dur = null; | |
32 | + liaisonref = null; | |
33 | + stressref = null; | |
34 | + senseref = null; | |
35 | + start = null; | |
36 | + liaisonscore = null; | |
37 | + fluency = null; | |
38 | + toneref = null; | |
39 | + stressscore = null; | |
40 | + end = null; | |
41 | + sensescore = null; | |
42 | + } | |
43 | + | |
44 | + factory SingsoundResultDetailEntity.fromJson(Map<String, dynamic> json) => $SingsoundResultDetailEntityFromJson(json); | |
45 | + | |
46 | + Map<String, dynamic> toJson() => $SingsoundResultDetailEntityToJson(this); | |
47 | + | |
48 | + @override | |
49 | + String toString() { | |
50 | + return jsonEncode(this); | |
51 | + } | |
52 | +} | |
0 | 53 | \ No newline at end of file | ... | ... |
lib/pages/reading/bloc/reading_bloc.dart
... | ... | @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; |
4 | 4 | import 'package:flutter/services.dart'; |
5 | 5 | import 'package:flutter_bloc/flutter_bloc.dart'; |
6 | 6 | import 'package:flutter_easyloading/flutter_easyloading.dart'; |
7 | +import 'package:flutter_screenutil/flutter_screenutil.dart'; | |
7 | 8 | import 'package:permission_handler/permission_handler.dart'; |
8 | 9 | import 'package:wow_english/common/extension/string_extension.dart'; |
9 | 10 | import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; |
... | ... | @@ -18,6 +19,7 @@ import '../../../common/request/exception.dart'; |
18 | 19 | import '../../../common/utils/click_with_music_controller.dart'; |
19 | 20 | import '../../../common/utils/show_star_reward_dialog.dart'; |
20 | 21 | import '../../../models/course_process_entity.dart'; |
22 | +import '../../../models/singsound_result_detail_entity.dart'; | |
21 | 23 | import '../../../models/voice_result_type.dart'; |
22 | 24 | import '../../../route/route.dart'; |
23 | 25 | import '../../../utils/loading.dart'; |
... | ... | @@ -308,6 +310,41 @@ class ReadingPageBloc |
308 | 310 | return currentPageData()?.word?.trim() ?? ''; |
309 | 311 | } |
310 | 312 | |
313 | + /// 结合单词分数返回带颜色的TextSpan数组 | |
314 | + List<TextSpan>? displayContent() { | |
315 | + // return _textController.text.isNotEmpty ? _textController.text : readingContent(); | |
316 | + // 创建用于展示每个单词的 TextSpan 列表 | |
317 | + if (currentPageData() == null) { | |
318 | + return null; | |
319 | + } | |
320 | + List<SingsoundResultDetailEntity> resultDetails; | |
321 | + if (currentPageData()?.resultDetails != null) { | |
322 | + resultDetails = currentPageData()!.resultDetails!; | |
323 | + } else { | |
324 | + List<String>? wordList = currentPageData()?.word?.split(RegExp(r'\s+')); | |
325 | + resultDetails = wordList! | |
326 | + .map( | |
327 | + (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) | |
328 | + .toList(); | |
329 | + } | |
330 | + List<TextSpan> textSpans = resultDetails.asMap().entries.map((entry) { | |
331 | + int index = entry.key; | |
332 | + SingsoundResultDetailEntity detail = entry.value; | |
333 | + // Check if this is the last word in the list | |
334 | + bool isLastWord = index == resultDetails.length - 1; | |
335 | + return TextSpan( | |
336 | + text: '${detail.char}${isLastWord ? '' : ' '}', | |
337 | + style: TextStyle( | |
338 | + color: detail.score > 80 | |
339 | + ? const Color(0XFF35C137) | |
340 | + : const Color(0xFF333333), | |
341 | + fontSize: 20.sp, | |
342 | + ), | |
343 | + ); | |
344 | + }).toList(); | |
345 | + return textSpans; | |
346 | + } | |
347 | + | |
311 | 348 | void nextPage() { |
312 | 349 | if (currentPage >= dataCount()) { |
313 | 350 | sectionComplete(() { |
... | ... | @@ -362,12 +399,23 @@ class ReadingPageBloc |
362 | 399 | final result = args['result'] as Map; |
363 | 400 | Log.d("_voiceXsResult result=$result"); |
364 | 401 | final overall = result['overall'].toString(); |
402 | + List<dynamic> resultDetailsJsons = result['details']; | |
403 | + // 提取 score 和 char 字段 | |
404 | + List<SingsoundResultDetailEntity> detailEntities = []; | |
405 | + for (var detail in resultDetailsJsons) { | |
406 | + int score = detail['score'] as int; | |
407 | + String char = detail['char'] as String; | |
408 | + detailEntities | |
409 | + .add(SingsoundResultDetailEntity.withCharAndScore(char, score)); | |
410 | + } | |
365 | 411 | |
366 | 412 | ///todo 后面可以考虑要不要传自己的服务器 |
367 | 413 | final recordFileUrl = args['audioUrl'].toString(); |
368 | 414 | int score = int.parse(overall); |
369 | 415 | currentPageData()?.recordScore = overall; |
370 | 416 | currentPageData()?.recordUrl = recordFileUrl.assetMp3; |
417 | + currentPageData()?.resultDetails = detailEntities; | |
418 | + add(OnXSVoiceStateChangeEvent()); | |
371 | 419 | |
372 | 420 | final voiceResult = VoiceResultType.fromScore(score); |
373 | 421 | if (voiceResult.lottieFilePath != null) { | ... | ... |
lib/pages/reading/reading_page.dart
... | ... | @@ -154,7 +154,7 @@ class _ReadingPage extends StatelessWidget { |
154 | 154 | Align( |
155 | 155 | alignment: Alignment.bottomLeft, |
156 | 156 | child: Container( |
157 | - color: const Color(0x80FFFFFF), | |
157 | + color: const Color(0xCCFFFFFF), | |
158 | 158 | child: Row( |
159 | 159 | children: [ |
160 | 160 | 5.horizontalSpace, |
... | ... | @@ -175,13 +175,15 @@ class _ReadingPage extends StatelessWidget { |
175 | 175 | width: 10.w, |
176 | 176 | ), |
177 | 177 | Expanded( |
178 | - child: Text( | |
179 | - bloc.readingContent(), | |
180 | - style: TextStyle( | |
181 | - color: const Color(0xFF333333), fontSize: 21.sp), | |
182 | - maxLines: 2, | |
183 | - overflow: TextOverflow.ellipsis, | |
184 | - )), | |
178 | + child: RichText( | |
179 | + text: TextSpan( | |
180 | + style: DefaultTextStyle.of(context).style, | |
181 | + children: bloc.displayContent(), | |
182 | + ), | |
183 | + maxLines: 2, | |
184 | + overflow: TextOverflow.ellipsis, | |
185 | + ), | |
186 | + ), | |
185 | 187 | SizedBox( |
186 | 188 | width: 10.w, |
187 | 189 | ), | ... | ... |