Commit fd737d6ae275311fe88db3416e3dd53cbd382d9d

Authored by 吴启风
1 parent d0623cfd

feat:绘本作答结果以单词为单位根据分数展示不同颜色

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&lt;String, dynamic&gt; $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 &#39;package:wow_english/generated/json/base/json_field.dart&#39;;
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 &#39;package:flutter/foundation.dart&#39;;
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 &#39;../../../common/request/exception.dart&#39;;
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 ),
... ...