feat: 首页,收藏页的UI,字体 组件封装

This commit is contained in:
chengsiwen committed 2026-07-03 15:38:01 +08:00
1 parent f233f1c642
commit 7f329aef44
32 files changed
+1110 -41

No files matched your search

+402 -2
View File
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../../tools/widgets/app_bg_page.dart';
class CollectionPage extends StatefulWidget {
const CollectionPage({super.key});
@@ -10,9 +12,407 @@ class CollectionPage extends StatefulWidget {
}
class _RootPageState extends State<CollectionPage> {
static const List<_DramaItem> _favoriteItems = [
_DramaItem(
title: 'An Uninvited Guest',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/09/26/66f4d0ce67383.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Vanished Without a Word',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/06/11/6667f17c9df69.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Dawn of Stardom',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/07/19/669a15b8a2e28.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Marry Me Twice',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/04/30/6630c89b746a8.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Not Your Bride',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/05/22/664daf9d042d4.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: "Husband's Hidden Crown",
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/08/12/66b9dfd8182cc.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'In the City of Love',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/07/24/66a0b88fda163.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: "Mommy's Genius Squad",
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/05/17/664716f187b2b.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Burned by Love',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/07/05/66876b720daf4.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
];
static const List<_DramaItem> _historyItems = [
_DramaItem(
title: 'Shadows of Vengeance',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: "CEO's Naughty Wife Is Hidden Rich",
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/08/02/66accc37da3a5.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Her Veiled Protector',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/06/28/667e7312bc731.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Love Deep As The Ocean',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/06/14/666c06fb54983.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Not Your Bride',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/05/22/664daf9d042d4.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Shadows of Vengeance',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
_DramaItem(
title: 'Shadows of Vengeance',
imageUrl:
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
currentEpisode: 1,
totalEpisode: 51,
),
];
bool _showHistory = false;
@override
Widget build(BuildContext context) {
return Scaffold();
return AppBgPage(
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: _showHistory
? _buildHistoryList()
: _buildFavoritesGrid(),
),
),
],
),
),
);
}
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.fromLTRB(30, 18, 26, 12),
child: Row(
children: [
_buildTab(
'Favorites',
active: !_showHistory,
onTap: () {
setState(() {
_showHistory = false;
});
},
),
const SizedBox(width: 16),
_buildTab(
'History',
active: _showHistory,
onTap: () {
setState(() {
_showHistory = true;
});
},
),
const Spacer(),
const Icon(Icons.edit_outlined, color: Colors.white, size: 22),
],
),
);
}
Widget _buildTab(
String title, {
required bool active,
required VoidCallback onTap,
}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: active
? Colors.white
: Colors.white.withValues(alpha: 0.42),
fontSize: 16,
fontFamily: 'Montserrat-Bold',
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 5),
AnimatedContainer(
duration: const Duration(milliseconds: 180),
width: active ? 54 : 0,
height: 2,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(1),
),
),
],
),
);
}
Widget _buildFavoritesGrid() {
return GridView.builder(
key: const ValueKey('favorites'),
padding: const EdgeInsets.fromLTRB(30, 12, 14, 96),
itemCount: _favoriteItems.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 14,
childAspectRatio: 0.55,
),
itemBuilder: (context, index) {
return _FavoriteCard(item: _favoriteItems[index]);
},
);
}
Widget _buildHistoryList() {
return ListView.separated(
key: const ValueKey('history'),
padding: const EdgeInsets.fromLTRB(30, 8, 20, 96),
itemCount: _historyItems.length,
separatorBuilder: (context, index) => const SizedBox(height: 13),
itemBuilder: (context, index) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 30,
child: Icon(
Icons.bookmark_border,
color: Colors.white.withValues(alpha: 0.28),
size: 32,
),
),
const SizedBox(width: 18),
Expanded(child: _HistoryItem(item: _historyItems[index])),
],
);
},
);
}
}
class _FavoriteCard extends StatelessWidget {
const _FavoriteCard({required this.item});
final _DramaItem item;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.network(
item.imageUrl,
width: constraints.maxWidth,
height: constraints.maxWidth * 1.33,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: constraints.maxWidth,
height: constraints.maxWidth * 1.33,
color: Colors.white.withValues(alpha: 0.12),
);
},
),
),
const SizedBox(height: 8),
Text(
item.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
height: 1.18,
fontFamily: 'Montserrat-Bold',
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
item.episodeText,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.48),
fontSize: 11,
fontFamily: 'Montserrat-Medium',
fontWeight: FontWeight.w500,
),
),
],
);
},
);
}
}
class _HistoryItem extends StatelessWidget {
const _HistoryItem({required this.item});
final _DramaItem item;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 68,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.network(
item.imageUrl,
width: 51,
height: 68,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 51,
height: 68,
color: Colors.white.withValues(alpha: 0.12),
);
},
),
),
const SizedBox(width: 15),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.22,
fontFamily: 'Montserrat-Bold',
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text.rich(
TextSpan(
children: [
TextSpan(
text: 'EP.${item.currentEpisode}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
TextSpan(text: ' /EP.${item.totalEpisode}'),
],
),
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 12,
fontFamily: 'Montserrat-Medium',
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
}
class _DramaItem {
const _DramaItem({
required this.title,
required this.imageUrl,
required this.currentEpisode,
required this.totalEpisode,
});
final String title;
final String imageUrl;
final int currentEpisode;
final int totalEpisode;
String get episodeText => 'EP.$currentEpisode/EP.$totalEpisode';
}