merge: 收藏卡片展示简介
This commit is contained in:
commit
f2ae0c70b6
4 files changed
+117
-3
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
# 收藏卡片简介
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- 收藏卡片在标题/标签下方显示后端 `synopsis`,最多 2 行并省略溢出。
|
||||||
|
- 空白简介不创建文本节点、不占高度,不显示 `null/undefined`。
|
||||||
|
- 保持封面 80x106 和现有卡片高度,不引入布局溢出。
|
||||||
|
|
||||||
|
## 实现
|
||||||
|
|
||||||
|
1. `_FavoriteCard` 对 synopsis 做 trim 后条件渲染。
|
||||||
|
2. 简介使用 `Flexible` 占据标题、标签与底部统计之间的剩余空间,
|
||||||
|
`maxLines: 2`、`TextOverflow.ellipsis`、次要文案色和字号。
|
||||||
|
3. 无简介时继续使用原 Spacer,保持原布局。
|
||||||
|
4. widget 测试覆盖有简介、空简介和卡片高度稳定性。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 不改接口、后端、收藏分页。
|
||||||
|
- 不新增依赖,不提交现有 `pubspec.lock`,不启停服务、不操作真机。
|
||||||
@@ -698,3 +698,35 @@ $ flutter test --no-pub
|
|||||||
解读:有 dramaTitle 时不触发详情请求、点击时才加载详情、空标题兜底、
|
解读:有 dramaTitle 时不触发详情请求、点击时才加载详情、空标题兜底、
|
||||||
第 2 页追加去重和 hasMore 行为均通过;全量分析零告警,630 项通过、1 项旧服务
|
第 2 页追加去重和 hasMore 行为均通过;全量分析零告警,630 项通过、1 项旧服务
|
||||||
能力探测跳过。`pubspec.lock` 既有改动未提交。
|
能力探测跳过。`pubspec.lock` 既有改动未提交。
|
||||||
|
|
||||||
|
## 2026-10-22 收藏卡片简介
|
||||||
|
|
||||||
|
- 分支:`feature/favorite-card-synopsis`
|
||||||
|
- 基线:`develop` / `origin/develop` = `9093700`
|
||||||
|
- 计划:`docs/plans/2026-10-22-favorite-card-synopsis.md`
|
||||||
|
|
||||||
|
### 实现
|
||||||
|
|
||||||
|
- `_FavoriteCard` 在标题/标签下方条件渲染 `drama.synopsis.trim()`。
|
||||||
|
- 简介最多 2 行、`TextOverflow.ellipsis`,使用 `AppColors.muted`、12 号字和
|
||||||
|
`1.15` 行高,由 `Flexible` 占据剩余空间,保持封面与卡片原高度。
|
||||||
|
- 简介为空或纯空白时不创建文本节点,继续使用原 Spacer,不占高度。
|
||||||
|
- 未改接口、后端和收藏分页逻辑。
|
||||||
|
|
||||||
|
### 门禁
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ flutter test --no-pub test/library/favorites_page_test.dart
|
||||||
|
00:01 +7: All tests passed!
|
||||||
|
|
||||||
|
$ dart analyze lib test
|
||||||
|
Analyzing lib, test...
|
||||||
|
No issues found!
|
||||||
|
|
||||||
|
$ flutter test --no-pub
|
||||||
|
00:51 +631 ~1: All tests passed!
|
||||||
|
```
|
||||||
|
|
||||||
|
解读:有简介时文本最多 2 行并省略,空简介不渲染且卡片高度与正常卡片一致;
|
||||||
|
全量静态分析零告警,631 项通过、1 项运行中旧后端能力探测跳过。
|
||||||
|
`pubspec.lock` 既有改动未提交。
|
||||||
@@ -34,6 +34,9 @@ const double _removeIconSize = 20;
|
|||||||
const double _statsFontSize = 12;
|
const double _statsFontSize = 12;
|
||||||
const double _metaFontSize = 11;
|
const double _metaFontSize = 11;
|
||||||
const double _titleFontSize = 15;
|
const double _titleFontSize = 15;
|
||||||
|
const double _synopsisFontSize = 12;
|
||||||
|
const double _synopsisLineHeight = 1.15;
|
||||||
|
const double _synopsisGap = 4;
|
||||||
const int _maxTags = 2;
|
const int _maxTags = 2;
|
||||||
const double _emptyListHeight = 360;
|
const double _emptyListHeight = 360;
|
||||||
const double _loadMoreThreshold = 240;
|
const double _loadMoreThreshold = 240;
|
||||||
@@ -252,6 +255,7 @@ class _FavoriteCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final tags = _tags();
|
final tags = _tags();
|
||||||
|
final synopsis = drama.synopsis.trim();
|
||||||
return Container(
|
return Container(
|
||||||
key: ValueKey('favorite-card-${drama.id}'),
|
key: ValueKey('favorite-card-${drama.id}'),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -345,7 +349,25 @@ class _FavoriteCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (synopsis.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: _synopsisGap),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
synopsis,
|
||||||
|
key: ValueKey(
|
||||||
|
'favorite-synopsis-${drama.id}',
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.muted,
|
||||||
|
fontSize: _synopsisFontSize,
|
||||||
|
height: _synopsisLineHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else
|
||||||
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ import '../support/fake_interaction_repositories.dart';
|
|||||||
import '../support/fake_notice_repository.dart';
|
import '../support/fake_notice_repository.dart';
|
||||||
import '../support/widget_player.dart';
|
import '../support/widget_player.dart';
|
||||||
|
|
||||||
Drama _card(String id) => Drama(
|
Drama _card(String id, {String synopsis = 'synopsis'}) => Drama(
|
||||||
id: id,
|
id: id,
|
||||||
title: 'Drama $id',
|
title: 'Drama $id',
|
||||||
synopsis: 'synopsis',
|
synopsis: synopsis,
|
||||||
posterUrl: '/files/$id.png',
|
posterUrl: '/files/$id.png',
|
||||||
totalEpisodes: 12,
|
totalEpisodes: 12,
|
||||||
);
|
);
|
||||||
@@ -73,6 +73,14 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(title.style?.color, AppColors.onSurface);
|
expect(title.style?.color, AppColors.onSurface);
|
||||||
expect(title.style?.fontSize, 15);
|
expect(title.style?.fontSize, 15);
|
||||||
|
final synopsis = tester.widget<Text>(
|
||||||
|
find.byKey(const Key('favorite-synopsis-signal')),
|
||||||
|
);
|
||||||
|
expect(synopsis.data, 'synopsis');
|
||||||
|
expect(synopsis.maxLines, 2);
|
||||||
|
expect(synopsis.overflow, TextOverflow.ellipsis);
|
||||||
|
expect(synopsis.style?.color, AppColors.muted);
|
||||||
|
expect(synopsis.style?.fontSize, 12);
|
||||||
expect(
|
expect(
|
||||||
tester.getSize(find.byKey(const Key('favorite-cover-signal'))),
|
tester.getSize(find.byKey(const Key('favorite-cover-signal'))),
|
||||||
const Size(80, 106),
|
const Size(80, 106),
|
||||||
@@ -100,6 +108,38 @@ void main() {
|
|||||||
expect(find.byKey(const Key('favorite-card-signal')), findsNothing);
|
expect(find.byKey(const Key('favorite-card-signal')), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_hide_blank_synopsis_without_changing_card_height', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final auth = await _auth(signedIn: true);
|
||||||
|
final longSynopsis = List.filled(12, '很长的剧情简介').join();
|
||||||
|
final favorites = FavoriteController(
|
||||||
|
InMemoryFavoriteRepository(
|
||||||
|
saved: {'with-synopsis', 'without-synopsis'},
|
||||||
|
cards: [
|
||||||
|
_card('with-synopsis', synopsis: longSynopsis),
|
||||||
|
_card('without-synopsis', synopsis: ' '),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pumpPage(tester, auth: auth, favorites: favorites);
|
||||||
|
|
||||||
|
final withSynopsis = tester.widget<Text>(
|
||||||
|
find.byKey(const Key('favorite-synopsis-with-synopsis')),
|
||||||
|
);
|
||||||
|
expect(withSynopsis.maxLines, 2);
|
||||||
|
expect(withSynopsis.overflow, TextOverflow.ellipsis);
|
||||||
|
expect(
|
||||||
|
find.byKey(const Key('favorite-synopsis-without-synopsis')),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byKey(const Key('favorite-card-with-synopsis'))),
|
||||||
|
tester.getSize(find.byKey(const Key('favorite-card-without-synopsis'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_keep_card_and_show_failure_when_remove_fails', (
|
testWidgets('should_keep_card_and_show_failure_when_remove_fails', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in new issue
Block a user