Skip to content

Commit ed4e3f1

Browse files
committed
STYLE: 优化意群卡片背景与边框
1 parent 3ba8180 commit ed4e3f1

3 files changed

Lines changed: 85 additions & 26 deletions

File tree

‎TASKS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
## 本次完成
66

7+
- [x] 提高夜间意群边框可见度(2026-10-17):暗色主题下普通态与已播放态边框不透明度从 0.3 提高到 0.6;亮色主题、播放中状态和收藏态保持原样。新增暗色主题边框回归测试;意群组件定向测试 10 项通过,相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。未运行 Maestro:现有 Flow 会 `clearState` 且不覆盖该组件;未运行 `scripts/check.sh`:改动局限于意群边框样式,定向验证已覆盖。**完成时间**: 2026-10-17 12:48
8+
- [x] 移除未收藏意群普通态与播放态的背景填充(2026-10-17):普通态与播放态改为透明底,继续由边框颜色区分;已收藏意群保留橙色背景。新增组件回归测试,普通态和播放态无填充且边框不同;定向 widget 测试 9 项通过,相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。未运行 Maestro:现有 Flow 均会 `clearState` 且不覆盖意群组件,为避免清除设备应用数据而跳过;未运行 `scripts/check.sh`:改动局限于意群 badge 样式,定向验证已覆盖。**完成时间**: 2026-10-17 12:37
79
- [x] 精听音频与视频统一到 MediaEngine(2026-10-17):音频和视频精听共用 `MediaSentencePlaybackDriver`、媒体加载/重试/取消入口及页面加载状态;仅视频显示画面,保留音频普通加载反馈。进入时暂停并移交 Free Player 监听,失败、取消和退出时按会话生命周期恢复;保留断点、收藏、意群试听和自动补做语义。补充结构化日志,覆盖入口、材料类型、断点、媒体 session、逐句索引/区间/速度/结果/耗时、停顿、暂停/续播/切句、取消和退出,不记录句子正文;修正冷启动日志将空 backend 显示为 `uninitialized`。Provider、状态机、页面和 MediaEngine 定向测试分别通过(38 项通过/4 项跳过、54 项、50 项、22 项),相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。Android dev 包构建安装成功;SM A065F 上无清理数据的 Maestro 启动 Flow 通过。未运行 `scripts/check.sh`:改动范围局限于精听播放链路,定向测试、分析和设备启动验证已覆盖受影响代码。**完成时间**: 2026-10-17 11:29
810
- [x] 复核并收敛精听播放链路命名(2026-10-17):播放链路枚举改为 `audioEngine` / `mediaEngine`,精听状态以 `hasVideo` 表达画面条件,注解会话与 MediaEngine 播放会话使用各自明确的 ID 名称;精听生产驱动移除旧 `AudioEngine` 适配器和媒体驱动旧别名,把可控音频替身移到测试文件。修正失败分支残留旧局部变量名。17 个 Dart 源码/测试文件定向 `flutter analyze --no-pub` 通过;精听 Provider、MediaEngine、学习会话、学习计划、盲听、复述及难句补练相关测试共 232 项通过、4 项跳过,`git diff --check` 通过。未运行 `scripts/check.sh`:此次仅收敛命名和测试替身边界,定向分析与测试覆盖了受影响接口。**完成时间**: 2026-10-17 11:53
911
- [x] 复核精听音视频实机播放日志(2026-10-17):`flutter_run.log` 12:15 音频精听和 12:17 视频精听均通过 MediaEngine 加载,并由 `MediaSentencePlayback` 发起区间播放;自然完成、暂停取消、快速切句后的旧回调忽略均符合预期,本次日志范围内未见旧 AudioEngine 播放或异常。视频最后暂停后 App 转入后台,未记录视频会话的退出释放,因此本次只确认了视频播放链路。**完成时间**: 2026-10-17 12:21

‎lib/widgets/practice/sense_group_text.dart‎

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/// 意群标注文本组件
22
///
33
/// 将句子按意群渲染为内联 badge 样式,保持自然文本排版。
4-
/// 所有意群使用统一背景色,可点击播放对应音频片段。
4+
/// 未收藏意群使用透明背景,以边框区分播放状态;已收藏意群保留橙色背景。
5+
/// 所有意群均可点击播放对应音频片段。
56
/// 支持四种视觉状态:空闲 / 播放中 / 已播放 / 已收藏。
67
///
78
/// 已收藏的单词/词组在 badge 内文本上渲染橙色点状下划线(与
@@ -28,20 +29,6 @@ const _matchedColor = Color(0xFF2E9B51);
2829
final _englishWordPattern = RegExp(r"[A-Za-z]+(?:'[A-Za-z]+)?");
2930
final _playbackWordPattern = RegExp(r'\S+');
3031

31-
/// 意群 badge 空闲态背景色(亮色主题)
32-
///
33-
/// 用中性灰,搭配跟读完成后的绿色匹配字,也避免蓝底误导用户。
34-
const _groupColorLight = Color(0xFFEDEFF2); // 中性浅灰
35-
36-
/// 意群 badge 空闲态背景色(暗色主题)
37-
const _groupColorDark = Color(0xFF26262A); // 中性深灰
38-
39-
/// 播放中意群背景色(暗色主题)
40-
///
41-
/// 比空闲态更亮一档的实色蓝,凸显"正在播放";替代 M3 primaryContainer
42-
/// (低饱和灰蓝在纯黑上发雾),与空闲态保持同色系。
43-
const _playingColorDark = Color(0xFF1E4A70); // 亮蓝(播放中)
44-
4532
/// 已收藏意群背景色(亮色主题)
4633
final _savedColorLight = Colors.orange.shade50;
4734

@@ -339,16 +326,14 @@ class _SenseGroupTextState extends ConsumerState<SenseGroupText> {
339326
normalizeSenseGroupPhrase(chunk),
340327
);
341328
final isDark = Theme.of(context).brightness == Brightness.dark;
329+
final borderAlpha = isDark ? 0.6 : 0.3;
342330

343-
// 背景色优先级:播放中 > 已收藏 > 默认
344-
final Color bgColor;
345-
if (isPlaying) {
346-
bgColor = isDark ? _playingColorDark : colorScheme.primaryContainer;
347-
} else if (isSaved) {
348-
bgColor = isDark ? _savedColorDark : _savedColorLight;
349-
} else {
350-
bgColor = isDark ? _groupColorDark : _groupColorLight;
351-
}
331+
// 普通态和播放态保持透明底;仅非播放中的已收藏意群保留橙色提示。
332+
final Color? bgColor = isSaved && !isPlaying
333+
? isDark
334+
? _savedColorDark
335+
: _savedColorLight
336+
: null;
352337

353338
// 边框优先级:播放中 > 已收藏 > 已播放 > 默认
354339
final Color borderColor;
@@ -357,9 +342,9 @@ class _SenseGroupTextState extends ConsumerState<SenseGroupText> {
357342
} else if (isSaved) {
358343
borderColor = _savedBorderColor;
359344
} else if (isPlayed) {
360-
borderColor = colorScheme.primary.withValues(alpha: 0.3);
345+
borderColor = colorScheme.primary.withValues(alpha: borderAlpha);
361346
} else {
362-
borderColor = colorScheme.outline.withValues(alpha: 0.3);
347+
borderColor = colorScheme.outline.withValues(alpha: borderAlpha);
363348
}
364349
final border = Border.all(color: borderColor, width: 1.5);
365350

‎test/widgets/sense_group_text_test.dart‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_test/flutter_test.dart';
33
import 'package:echo_loop/models/speech_practice_models.dart';
44
import 'package:echo_loop/providers/saved_word_provider.dart';
5+
import 'package:echo_loop/theme/app_theme.dart';
56
import 'package:echo_loop/utils/saved_text_index.dart';
67
import 'package:echo_loop/widgets/practice/sense_group_text.dart';
78

@@ -34,6 +35,77 @@ void main() {
3435
SpeechTranscriptSegment word(String text, {required bool matched}) =>
3536
SpeechTranscriptSegment(text: text, isMatched: matched);
3637

38+
BoxDecoration badgeDecorationFor(WidgetTester tester, String text) {
39+
final badge = tester.widget<Container>(
40+
find
41+
.ancestor(of: find.text(text), matching: find.byType(Container))
42+
.first,
43+
);
44+
return switch (badge.decoration) {
45+
BoxDecoration decoration => decoration,
46+
_ => throw StateError('意群 badge 应使用 BoxDecoration'),
47+
};
48+
}
49+
50+
Color badgeBorderColor(BoxDecoration decoration) {
51+
return switch (decoration.border) {
52+
Border border => border.top.color,
53+
_ => throw StateError('意群 badge 应使用 Border'),
54+
};
55+
}
56+
57+
testWidgets('普通态与播放态意群无背景填充,播放态仍由边框区分', (tester) async {
58+
await tester.pumpWidget(
59+
createTestApp(
60+
SenseGroupText(
61+
chunks: const ['idle group', 'playing group'],
62+
timings: const [],
63+
onTapGroup: (_) {},
64+
playingGroupIndex: 1,
65+
),
66+
),
67+
);
68+
69+
final idleDecoration = badgeDecorationFor(tester, 'idle group');
70+
final playingDecoration = badgeDecorationFor(tester, 'playing group');
71+
72+
expect(idleDecoration.color, isNull);
73+
expect(playingDecoration.color, isNull);
74+
expect(idleDecoration.border, isNot(equals(playingDecoration.border)));
75+
});
76+
77+
testWidgets('暗色主题提高普通态与已播放态边框的可见度', (tester) async {
78+
await tester.pumpWidget(
79+
createTestApp(
80+
Theme(
81+
data: AppTheme.dark(),
82+
child: Scaffold(
83+
backgroundColor: Colors.black,
84+
body: SenseGroupText(
85+
chunks: const ['idle group', 'played group'],
86+
timings: const [],
87+
onTapGroup: (_) {},
88+
playedGroupIndices: const {1},
89+
),
90+
),
91+
),
92+
),
93+
);
94+
95+
final idleDecoration = badgeDecorationFor(tester, 'idle group');
96+
final playedDecoration = badgeDecorationFor(tester, 'played group');
97+
final darkColorScheme = AppTheme.dark().colorScheme;
98+
99+
expect(
100+
badgeBorderColor(idleDecoration),
101+
darkColorScheme.outline.withValues(alpha: 0.6),
102+
);
103+
expect(
104+
badgeBorderColor(playedDecoration),
105+
darkColorScheme.primary.withValues(alpha: 0.6),
106+
);
107+
});
108+
37109
group('SenseGroupText — 意群模式单词高亮', () {
38110
testWidgets('匹配单词染绿,未匹配单词不染绿', (tester) async {
39111
// 原文 "Hello brave world" → 意群拆为 ["Hello brave", "world"]

0 commit comments

Comments
 (0)