From 83129acae40f26cdf2079242838c9b0116ffcc73 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 17:07:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(mine):=20=E7=BB=9F=E4=B8=80=E6=88=91?= =?UTF-8?q?=E7=9A=84=E9=A1=B5=E6=B7=B1=E8=89=B2=E8=A7=86=E8=A7=89=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/theme/app_colors.dart | 1 + lib/core/theme/app_dimens.dart | 6 ++ lib/pages/mine/mine_view.dart | 45 ++++++---- lib/pages/mine/widgets/mine_header.dart | 21 +++-- lib/pages/mine/widgets/mine_menu.dart | 113 +++++++++++++----------- test/mine/mine_preview_layout_test.dart | 104 ++++++++++++++++++++++ 6 files changed, 208 insertions(+), 82 deletions(-) diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 83b62b3..6d2e57e 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -70,6 +70,7 @@ abstract final class AppColors { static const contentBorder = Color(0x1AFFFFFF); /// 次级文字与列表 chevron。 + static const textPrimary = Color(0xFFFFFFFF); static const textSecondary = Color(0xFF8E8E93); /// 原生开屏与冷启动过渡底色。 diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index f783c56..64f20ab 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -87,4 +87,10 @@ abstract final class AppDimens { static const double homeSectionIndicator = 2; static const double homeIconSlot = 44; static const double homeHeaderIcon = 22; + + /// 「我的」页中性视觉。 + static const double mineCardRadius = 16; + static const double mineRowHeight = 52; + static const double minePageTitle = 21; + static const double mineSectionTitle = 15; } diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index 7c83f61..a3e91da 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -80,21 +80,25 @@ class _MyPageState extends State { Widget _page(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final content = SafeArea( - bottom: false, - child: Column( - children: [ - _topBar(l10n), - Expanded(child: _body(context, l10n)), - if (!embedded) _bottomNav(), - ], + final content = ColoredBox( + key: const Key('mine-page-surface'), + color: AppColors.appBackground, + child: SafeArea( + bottom: false, + child: Column( + children: [ + _topBar(l10n), + Expanded(child: _body(context, l10n)), + if (!embedded) _bottomNav(), + ], + ), ), ); return Theme( data: AppTheme.dark(), child: embedded ? content - : Scaffold(backgroundColor: AppColors.ink, body: content), + : Scaffold(backgroundColor: AppColors.appBackground, body: content), ); } @@ -111,9 +115,9 @@ class _MyPageState extends State { l10n.myTab, style: const TextStyle( fontFamily: 'Outfit', - color: AppColors.onSurface, - fontSize: AppDimens.pageTitle, - fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + fontSize: AppDimens.minePageTitle, + fontWeight: FontWeight.w700, ), ), ), @@ -280,8 +284,10 @@ class _LanguageSheet extends StatelessWidget { ]; return Material( key: const Key('mine-language-sheet'), - color: AppColors.card, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + color: AppColors.contentCard, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppDimens.mineCardRadius), + ), clipBehavior: Clip.antiAlias, child: SafeArea( top: false, @@ -297,9 +303,9 @@ class _LanguageSheet extends StatelessWidget { child: Text( l10n.language, style: const TextStyle( - color: AppColors.onSurface, + color: AppColors.textPrimary, fontSize: 20, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w700, ), ), ), @@ -316,7 +322,10 @@ class _LanguageSheet extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 4), title: Text(option.$3), trailing: selected?.languageCode == option.$1.languageCode - ? const Icon(Icons.check_rounded, color: AppColors.blue) + ? const Icon( + Icons.check_rounded, + color: AppColors.textPrimary, + ) : null, onTap: () => Navigator.pop(context, option.$1), ), @@ -351,7 +360,7 @@ class _MinePlaceholderPage extends StatelessWidget { child: Text( l10n.comingSoon, style: const TextStyle( - color: AppColors.muted, + color: AppColors.textSecondary, fontSize: AppDimens.body, ), ), diff --git a/lib/pages/mine/widgets/mine_header.dart b/lib/pages/mine/widgets/mine_header.dart index 9b80dbf..91b8bd2 100644 --- a/lib/pages/mine/widgets/mine_header.dart +++ b/lib/pages/mine/widgets/mine_header.dart @@ -6,7 +6,6 @@ import 'package:provider/provider.dart'; import '../../../core/data/services/auth_controller.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; -import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; @@ -31,13 +30,13 @@ class MineHeader extends StatelessWidget { return Container( key: const Key('mine-user-row'), decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - boxShadow: const [AppTheme.cardShadow], + color: AppColors.contentCard, + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), + border: Border.all(color: AppColors.contentBorder), ), child: Material( color: AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, @@ -50,7 +49,7 @@ class MineHeader extends StatelessWidget { Expanded(child: _labels(name, subtitle)), const Icon( Icons.chevron_right_rounded, - color: AppColors.subtle, + color: AppColors.textSecondary, size: 20, ), ], @@ -67,14 +66,14 @@ class MineHeader extends StatelessWidget { width: 64, height: 64, decoration: const BoxDecoration( - color: AppColors.badgePink, + color: AppColors.contentSurface, shape: BoxShape.circle, ), clipBehavior: Clip.antiAlias, child: path.isEmpty ? const Icon( Icons.person_rounded, - color: AppColors.badgePinkText, + color: AppColors.textPrimary, size: 30, ) : RemoteImage(path, fit: BoxFit.cover), @@ -91,9 +90,9 @@ class MineHeader extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: 'Outfit', - color: AppColors.onSurface, + color: AppColors.textPrimary, fontSize: 18, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppDimens.spaceXs), @@ -102,7 +101,7 @@ class MineHeader extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.muted, + color: AppColors.textSecondary, fontSize: AppDimens.caption, ), ), diff --git a/lib/pages/mine/widgets/mine_menu.dart b/lib/pages/mine/widgets/mine_menu.dart index 7eab07a..b1dce67 100644 --- a/lib/pages/mine/widgets/mine_menu.dart +++ b/lib/pages/mine/widgets/mine_menu.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; -import '../../../core/theme/app_theme.dart'; import '../../../l10n/app_localizations.dart'; import '../mine_types.dart'; @@ -22,12 +21,12 @@ class MineShortcutGrid extends StatelessWidget { Expanded( child: _ShortcutTile( key: const Key('mine-watchlist'), - icon: Icons.bookmark_rounded, + icon: Icons.bookmark_border_rounded, label: l10n.watchlist, onTap: () => onOpen(MineDestination.watchlist), ), ), - const SizedBox(width: 10), + const SizedBox(width: AppDimens.homeGridGap), Expanded( child: _ShortcutTile( key: const Key('mine-history'), @@ -36,11 +35,11 @@ class MineShortcutGrid extends StatelessWidget { onTap: () => onOpen(MineDestination.history), ), ), - const SizedBox(width: 10), + const SizedBox(width: AppDimens.homeGridGap), Expanded( child: _ShortcutTile( key: const Key('mine-downloads'), - icon: Icons.download_rounded, + icon: Icons.download_outlined, label: l10n.downloads, onTap: () => onOpen(MineDestination.downloads), ), @@ -66,13 +65,13 @@ class _ShortcutTile extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - boxShadow: const [AppTheme.cardShadow], + color: AppColors.contentCard, + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), + border: Border.all(color: AppColors.contentBorder), ), child: Material( color: AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, @@ -81,16 +80,16 @@ class _ShortcutTile extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: AppColors.blue, size: 24), + Icon(icon, color: AppColors.textPrimary, size: 24), const SizedBox(height: AppDimens.spaceSm), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.onSurface, + color: AppColors.textPrimary, fontSize: AppDimens.caption, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, ), ), ], @@ -122,20 +121,16 @@ class MineWalletCard extends StatelessWidget { key: const Key('mine-wallet'), padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppColors.goldCard, AppColors.card], - ), - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - boxShadow: const [AppTheme.cardShadow], + color: AppColors.contentCard, + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), + border: Border.all(color: AppColors.contentBorder), ), child: Row( children: [ const Icon( - Icons.monetization_on_rounded, - color: AppColors.gold, - size: 30, + Icons.toll_outlined, + color: AppColors.textSecondary, + size: 28, ), const SizedBox(width: AppDimens.spaceMd), Expanded( @@ -147,7 +142,7 @@ class MineWalletCard extends StatelessWidget { Text( l10n.coinBalance, style: const TextStyle( - color: AppColors.muted, + color: AppColors.textSecondary, fontSize: AppDimens.caption, ), ), @@ -155,9 +150,9 @@ class MineWalletCard extends StatelessWidget { Text( balance, style: const TextStyle( - color: AppColors.onSurface, + color: AppColors.textPrimary, fontSize: AppDimens.pageTitle, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w700, ), ), ], @@ -171,7 +166,9 @@ class MineWalletCard extends StatelessWidget { backgroundColor: AppColors.blue, foregroundColor: AppColors.onPrimary, minimumSize: const Size(64, AppDimens.buttonCompact), - shape: const StadiumBorder(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppDimens.buttonRadius), + ), ), child: Text(l10n.topUpWallet), ), @@ -193,13 +190,13 @@ class MineVipCard extends StatelessWidget { return Container( key: const Key('mine-vip'), decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - boxShadow: const [AppTheme.cardShadow], + color: AppColors.contentCard, + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), + border: Border.all(color: AppColors.contentBorder), ), child: Material( color: AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onOpen, @@ -211,12 +208,12 @@ class MineVipCard extends StatelessWidget { width: AppDimens.buttonLarge, height: AppDimens.buttonLarge, decoration: const BoxDecoration( - color: AppColors.badgePink, + color: AppColors.contentSurface, shape: BoxShape.circle, ), child: const Icon( Icons.workspace_premium_rounded, - color: AppColors.badgePinkText, + color: AppColors.textPrimary, size: 22, ), ), @@ -228,9 +225,9 @@ class MineVipCard extends StatelessWidget { Text( l10n.vipMembership, style: const TextStyle( - color: AppColors.onSurface, - fontSize: AppDimens.listTitle, - fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + fontSize: AppDimens.mineSectionTitle, + fontWeight: FontWeight.w600, ), ), const SizedBox(height: 3), @@ -239,7 +236,7 @@ class MineVipCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.muted, + color: AppColors.textSecondary, fontSize: AppDimens.caption, ), ), @@ -250,7 +247,7 @@ class MineVipCard extends StatelessWidget { l10n.joinVip, style: const TextStyle( color: AppColors.blue, - fontSize: AppDimens.caption, + fontSize: AppDimens.body, fontWeight: FontWeight.w700, ), ), @@ -282,13 +279,13 @@ class MineMenuList extends StatelessWidget { return Container( key: const Key('mine-menu'), decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - boxShadow: const [AppTheme.cardShadow], + color: AppColors.contentCard, + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), + border: Border.all(color: AppColors.contentBorder), ), child: Material( color: AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(AppDimens.mineCardRadius), clipBehavior: Clip.antiAlias, child: Column( children: [ @@ -338,18 +335,19 @@ class MineMenuList extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return ListTile( key: const Key('mine-language'), - minTileHeight: 48, + minTileHeight: AppDimens.mineRowHeight, contentPadding: const EdgeInsets.symmetric(horizontal: 14), leading: const Icon( Icons.language_rounded, - color: AppColors.blue, + color: AppColors.textSecondary, size: 20, ), title: Text( l10n.language, style: const TextStyle( - color: AppColors.onSurface, - fontSize: AppDimens.body, + color: AppColors.textPrimary, + fontSize: AppDimens.listTitle, + fontWeight: FontWeight.w500, ), ), trailing: Row( @@ -357,12 +355,15 @@ class MineMenuList extends StatelessWidget { children: [ Text( languageLabel, - style: const TextStyle(color: AppColors.muted, fontSize: 11), + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: AppDimens.caption, + ), ), const SizedBox(width: 4), const Icon( Icons.chevron_right_rounded, - color: AppColors.subtle, + color: AppColors.textSecondary, size: 18, ), ], @@ -379,19 +380,20 @@ class MineMenuList extends StatelessWidget { }) { return ListTile( key: key, - minTileHeight: 48, + minTileHeight: AppDimens.mineRowHeight, contentPadding: const EdgeInsets.symmetric(horizontal: 14), - leading: Icon(icon, color: AppColors.blue, size: 20), + leading: Icon(icon, color: AppColors.textSecondary, size: 20), title: Text( title, style: const TextStyle( - color: AppColors.onSurface, - fontSize: AppDimens.body, + color: AppColors.textPrimary, + fontSize: AppDimens.listTitle, + fontWeight: FontWeight.w500, ), ), trailing: const Icon( Icons.chevron_right_rounded, - color: AppColors.subtle, + color: AppColors.textSecondary, size: 18, ), onTap: () => onOpen(destination), @@ -399,6 +401,11 @@ class MineMenuList extends StatelessWidget { } Widget _divider() { - return const Divider(height: 1, indent: 14, endIndent: 14); + return const Divider( + height: 1, + indent: 14, + endIndent: 14, + color: AppColors.contentSurface, + ); } } diff --git a/test/mine/mine_preview_layout_test.dart b/test/mine/mine_preview_layout_test.dart index 62eb05a..5def2ce 100644 --- a/test/mine/mine_preview_layout_test.dart +++ b/test/mine/mine_preview_layout_test.dart @@ -6,6 +6,8 @@ import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; +import 'package:fruitshorts/core/theme/app_colors.dart'; +import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; @@ -69,6 +71,108 @@ void main() { expect(find.byKey(const Key('mine-library-tab-0')), findsNothing); expect(find.byKey(const Key('mine-library-tab-1')), findsNothing); expect(find.byKey(const Key('mine-library-tab-2')), findsNothing); + final surface = tester.widget( + find.byKey(const Key('mine-page-surface')), + ); + expect(surface.color, AppColors.appBackground); + final title = tester.widget( + find.descendant( + of: find.byKey(const Key('mine-page-surface')), + matching: find.text('My'), + ), + ); + expect(title.style?.color, AppColors.textPrimary); + expect(title.style?.fontSize, AppDimens.minePageTitle); + expect(title.style?.fontWeight, FontWeight.w700); + + for (final key in const [ + Key('mine-user-row'), + Key('mine-wallet'), + Key('mine-vip'), + Key('mine-menu'), + ]) { + final container = tester.widget(find.byKey(key)); + final decoration = container.decoration! as BoxDecoration; + expect(decoration.color, AppColors.contentCard); + expect( + decoration.borderRadius, + BorderRadius.circular(AppDimens.mineCardRadius), + ); + expect(decoration.gradient, isNull); + expect(decoration.boxShadow, isNull); + expect((decoration.border! as Border).top.color, AppColors.contentBorder); + } + + final shortcutIcon = tester.widget( + find.descendant( + of: find.byKey(const Key('mine-watchlist')), + matching: find.byType(Icon), + ), + ); + expect(shortcutIcon.size, 24); + expect(shortcutIcon.color, AppColors.textPrimary); + final shortcutKeys = [ + const Key('mine-watchlist'), + const Key('mine-history'), + const Key('mine-downloads'), + ]; + final shortcutWidths = shortcutKeys + .map((key) => tester.getSize(find.byKey(key)).width) + .toList(); + expect(shortcutWidths.toSet(), hasLength(1)); + final shortcutCard = tester.widget( + find + .descendant( + of: find.byKey(const Key('mine-watchlist')), + matching: find.byType(Container), + ) + .first, + ); + final shortcutDecoration = shortcutCard.decoration! as BoxDecoration; + expect(shortcutDecoration.color, AppColors.contentCard); + expect( + shortcutDecoration.borderRadius, + BorderRadius.circular(AppDimens.mineCardRadius), + ); + expect(shortcutDecoration.boxShadow, isNull); + final menuRows = find.descendant( + of: find.byKey(const Key('mine-menu')), + matching: find.byType(ListTile), + ); + expect(menuRows, findsNWidgets(6)); + for (final row in tester.widgetList(menuRows)) { + expect(row.minTileHeight, AppDimens.mineRowHeight); + } + final settingsRow = tester.widget( + find.byKey(const Key('mine-settings')), + ); + final settingsTitle = settingsRow.title! as Text; + expect(settingsTitle.style?.color, AppColors.textPrimary); + expect(settingsTitle.style?.fontSize, AppDimens.listTitle); + expect((settingsRow.trailing! as Icon).color, AppColors.textSecondary); + final recharge = tester.widget( + find.byKey(const Key('mine-wallet-recharge')), + ); + expect(recharge.style?.backgroundColor?.resolve(const {}), AppColors.blue); + expect( + tester.widget(find.text('Join VIP')).style?.color, + AppColors.blue, + ); + final dividers = tester.widgetList( + find.descendant( + of: find.byKey(const Key('mine-menu')), + matching: find.byType(Divider), + ), + ); + expect(dividers, hasLength(5)); + expect( + dividers.every((divider) => divider.color == AppColors.contentSurface), + isTrue, + ); expect(tester.takeException(), isNull); + + await tester.tap(find.byKey(const Key('mine-watchlist'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('favorites-page')), findsOneWidget); }); }