fix(ui): 打磨推荐追剧我的设置页间距与图标

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-28 16:36:51 +08:00
1 parent de7da64886
commit 5346be40a9
33 files changed
+1408 -811

No files matched your search

+31 -20
View File
@@ -1,6 +1,7 @@
// Author: fruit.com.cn
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -13,8 +14,7 @@ import '../../../core/widgets/centered_toast.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
const double _buttonHeight = 34;
const double _buttonRadius = 6;
const double _buttonHeight = 40;
/// 「我的」页用户区。
class MineHeader extends StatelessWidget {
@@ -35,7 +35,7 @@ class MineHeader extends StatelessWidget {
final uid = signedIn ? user?.uid.trim() ?? '' : '';
return Padding(
key: const Key('mine-user-row'),
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceMd),
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceSm),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -55,7 +55,7 @@ class MineHeader extends StatelessWidget {
fontFamily: 'Outfit',
color: AppColors.textPrimary,
fontSize: AppDimens.mineUserName,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w400,
height: AppDimens.mineUserNameLineHeight,
),
),
@@ -121,18 +121,29 @@ class MineHeader extends StatelessWidget {
key: const Key('mine-avatar'),
width: AppDimens.mineAvatar,
height: AppDimens.mineAvatar,
decoration: const BoxDecoration(
color: AppColors.contentSurface,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.textTertiary,
width: 1.2,
),
),
child: ClipOval(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
color: AppColors.textPrimary.withValues(alpha: 0.06),
alignment: Alignment.center,
child: path.isEmpty
? const Icon(
Icons.person_outline_rounded,
color: AppColors.textPrimary,
size: 30,
)
: RemoteImage(path, fit: BoxFit.cover),
),
),
),
clipBehavior: Clip.antiAlias,
child: path.isEmpty
? const Icon(
Icons.person_rounded,
color: AppColors.textPrimary,
size: 28,
)
: RemoteImage(path, fit: BoxFit.cover),
);
}
@@ -141,20 +152,20 @@ class MineHeader extends StatelessWidget {
required String label,
required VoidCallback onTap,
}) {
return OutlinedButton(
return TextButton(
key: key,
onPressed: onTap,
style: OutlinedButton.styleFrom(
style: TextButton.styleFrom(
minimumSize: const Size(0, _buttonHeight),
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.mineOutline),
backgroundColor: AppColors.brandRed,
foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_buttonRadius),
borderRadius: BorderRadius.circular(_buttonHeight / 2),
),
textStyle: const TextStyle(
fontSize: AppDimens.body,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w400,
),
),
child: Row(
+307 -215
View File
@@ -7,11 +7,9 @@ import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
import '../mine_types.dart';
const double _vipRadius = 12;
const double _vipBadgeSize = 36;
const double _vipButtonHeight = 34;
const double _vipRadius = 14;
/// 金色会员横幅。
/// 「我的」页金色 VIP 卡:品牌行 + 立即开通 + 2×2 权益宫格。
class MineVipBanner extends StatelessWidget {
const MineVipBanner({super.key, required this.onOpen});
@@ -24,7 +22,9 @@ class MineVipBanner extends StatelessWidget {
key: const Key('mine-vip'),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.vipGoldStart, AppColors.vipGoldEnd],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.vipCardStart, AppColors.vipCardEnd],
),
borderRadius: BorderRadius.circular(_vipRadius),
),
@@ -35,69 +35,89 @@ class MineVipBanner extends StatelessWidget {
child: InkWell(
onTap: onOpen,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.cardPadding,
vertical: AppDimens.spaceMd,
padding: const EdgeInsets.fromLTRB(
AppDimens.cardPadding,
AppDimens.spaceSm + 2,
AppDimens.cardPadding,
AppDimens.spaceSm + 2,
),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: _vipBadgeSize,
height: _vipBadgeSize,
decoration: const BoxDecoration(
color: AppColors.vipBadgeSurface,
shape: BoxShape.circle,
),
child: const Icon(
Icons.workspace_premium_rounded,
color: AppColors.onGold,
size: 22,
),
),
const SizedBox(width: AppDimens.spaceMd),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Row(
children: [
const Icon(
Icons.diamond_outlined,
color: AppColors.onGold,
size: 24,
),
const SizedBox(width: AppDimens.spaceSm),
Expanded(
child: Text(
l10n.vipBrandName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onGold,
fontSize: AppDimens.sectionTitle,
fontWeight: FontWeight.w700,
fontSize: 18,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 2),
Text(
l10n.mineVipSubtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onGold,
fontSize: AppDimens.caption,
),
),
],
),
const SizedBox(width: AppDimens.spaceSm),
_openButton(l10n),
],
),
Transform.translate(
offset: const Offset(0, -7),
child: Text(
l10n.mineVipSubtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: AppColors.onGold.withValues(alpha: 0.7),
fontSize: AppDimens.caption,
),
),
),
const SizedBox(width: AppDimens.spaceSm),
TextButton(
key: const Key('mine-vip-open'),
onPressed: onOpen,
style: TextButton.styleFrom(
minimumSize: const Size(0, _vipButtonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceMd,
const SizedBox(height: AppDimens.mineVipBenefitRowGap),
Container(
height: 1,
color: AppColors.onGold.withValues(alpha: 0.15),
),
const SizedBox(height: AppDimens.mineVipBenefitRowGap),
Table(
defaultColumnWidth: const FlexColumnWidth(),
children: [
TableRow(
children: [
_benefit(
key: const Key('mine-vip-benefit-0'),
icon: Icons.play_circle_outline_rounded,
label: l10n.vipBenefitFree,
),
_benefit(
key: const Key('mine-vip-benefit-1'),
icon: Icons.ads_click_rounded,
label: l10n.vipBenefitNoAds,
),
],
),
backgroundColor: AppColors.onGold,
foregroundColor: AppColors.textPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_vipButtonHeight),
TableRow(
children: [
_benefit(
key: const Key('mine-vip-benefit-2'),
icon: Icons.hd_outlined,
label: l10n.vipBenefitHd,
),
_benefit(
key: const Key('mine-vip-benefit-3'),
icon: Icons.download_for_offline_outlined,
label: l10n.vipBenefitFastCache,
),
],
),
),
child: Text(l10n.joinVip),
],
),
],
),
@@ -106,184 +126,256 @@ class MineVipBanner extends StatelessWidget {
),
);
}
Widget _openButton(AppLocalizations l10n) {
return TextButton(
key: const Key('mine-vip-open'),
onPressed: onOpen,
style: TextButton.styleFrom(
minimumSize: const Size(0, AppDimens.mineVipButtonHeight),
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
backgroundColor: AppColors.onGold,
foregroundColor: AppColors.vipGoldStart,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppDimens.mineVipButtonHeight / 2,
),
),
textStyle: const TextStyle(
fontSize: AppDimens.body,
fontWeight: FontWeight.w400,
),
),
child: Text(l10n.vipOpenNow),
);
}
Widget _benefit({required Key key, required IconData icon, required String label}) {
return Padding(
key: key,
padding: const EdgeInsets.symmetric(vertical: AppDimens.mineVipBenefitRowGap),
child: Row(
children: [
Container(
width: AppDimens.mineVipBenefitBox,
height: AppDimens.mineVipBenefitBox,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.onGold.withValues(alpha: 0.6),
width: 1.2,
),
borderRadius: BorderRadius.circular(
AppDimens.mineVipBenefitBoxRadius,
),
),
child: Icon(
icon,
color: AppColors.onGold,
size: AppDimens.mineVipBenefitGlyph,
),
),
const SizedBox(width: AppDimens.spaceSm),
Expanded(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onGold,
fontSize: AppDimens.mineVipBenefitFont,
),
),
),
],
),
);
}
}
/// 「我的」页 4 列功能入口。
class MineMenuList extends StatelessWidget {
const MineMenuList({
/// 快捷图标行:钱包 / 我的收藏 / 离线缓存 / 播放历史(设计稿的「奖励」暂不做)。
class MineQuickActions extends StatelessWidget {
const MineQuickActions({
super.key,
required this.onOpen,
required this.onLanguage,
required this.onWallet,
required this.onFavorites,
required this.onDownloads,
required this.onHistory,
});
final ValueChanged<MineDestination> onOpen;
final VoidCallback onLanguage;
final VoidCallback onWallet;
final VoidCallback onFavorites;
final VoidCallback onDownloads;
final VoidCallback onHistory;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
key: const Key('mine-menu'),
decoration: BoxDecoration(
color: AppColors.mineGridSurface,
borderRadius: BorderRadius.circular(AppDimens.mineGridRadius),
),
child: Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.mineGridRadius),
clipBehavior: Clip.antiAlias,
child: Table(
key: const Key('mine-menu-grid'),
defaultColumnWidth: const FlexColumnWidth(),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: const TableBorder(
horizontalInside: BorderSide(
color: AppColors.mineGridDivider,
width: AppDimens.mineGridDividerWidth,
),
verticalInside: BorderSide(
color: AppColors.mineGridDivider,
width: AppDimens.mineGridDividerWidth,
),
return Row(
children: [
_action(
key: const Key('mine-wallet'),
icon: Icons.wallet_outlined,
label: l10n.wallet,
onTap: onWallet,
),
_action(
key: const Key('mine-favorites'),
icon: Icons.star_border_rounded,
label: l10n.myFavorites,
onTap: onFavorites,
),
_action(
key: const Key('mine-downloads'),
icon: Icons.download_outlined,
label: l10n.offlineDownloads,
onTap: onDownloads,
),
_action(
key: const Key('mine-history'),
icon: Icons.history_rounded,
label: l10n.watchHistory,
onTap: onHistory,
),
],
);
}
Widget _action({
required Key key,
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: InkWell(
key: key,
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceSm),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: AppColors.textPrimary, size: AppDimens.mineQuickActionIcon),
const SizedBox(height: AppDimens.mineQuickActionGap),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.mineQuickActionLabelFont,
),
),
],
),
),
),
);
}
}
/// 「更多服务」列表行:白图标 + 标题 + 可选值 + chevron,行间细分隔线。
class MineServiceList extends StatelessWidget {
const MineServiceList({
super.key,
required this.onOpen,
required this.onLanguage,
required this.languageName,
});
final ValueChanged<MineDestination> onOpen;
final VoidCallback onLanguage;
final String languageName;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final rows = <Widget>[
_row(
key: const Key('mine-help'),
icon: Icons.headset_outlined,
label: l10n.contactSupport,
onTap: () => onOpen(MineDestination.help),
),
_divider(),
_row(
key: const Key('mine-language'),
icon: Icons.language_rounded,
label: l10n.language,
value: languageName,
onTap: onLanguage,
),
_divider(),
_row(
key: const Key('mine-settings'),
icon: Icons.settings_outlined,
label: l10n.settings,
onTap: () => onOpen(MineDestination.settings),
),
_divider(),
_row(
key: const Key('mine-notices'),
icon: Icons.notifications_none_rounded,
label: l10n.notices,
onTap: () => onOpen(MineDestination.notices),
),
];
return Column(key: const Key('mine-menu'), children: rows);
}
Widget _divider() => const Divider(
height: AppDimens.mineGridDividerWidth,
thickness: AppDimens.mineGridDividerWidth,
color: AppColors.contentBorder,
);
Widget _row({
required Key key,
required IconData icon,
required String label,
String? value,
required VoidCallback onTap,
}) {
return SizedBox(
height: AppDimens.mineServiceRowHeight,
child: InkWell(
key: key,
onTap: onTap,
child: Row(
children: [
TableRow(
children: [
_cell(
index: 0,
actionKey: const Key('mine-wallet'),
icon: Icons.account_balance_wallet_outlined,
iconColor: AppColors.mineIconWallet,
label: l10n.wallet,
onTap: () => onOpen(MineDestination.wallet),
Icon(icon, color: AppColors.textPrimary, size: AppDimens.mineServiceIcon),
const SizedBox(width: AppDimens.spaceMd),
Expanded(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.mineServiceTitleFont,
),
_cell(
index: 1,
actionKey: const Key('mine-downloads'),
icon: Icons.download_outlined,
iconColor: AppColors.mineIconDownloads,
label: l10n.offlineDownloads,
onTap: () => onOpen(MineDestination.downloads),
),
_cell(
index: 2,
actionKey: const Key('mine-history'),
icon: Icons.history_rounded,
iconColor: AppColors.mineIconHistory,
label: l10n.watchHistory,
onTap: () => onOpen(MineDestination.history),
),
_cell(
index: 3,
actionKey: const Key('mine-watch-preferences'),
icon: Icons.tune_rounded,
iconColor: AppColors.mineIconPreferences,
label: l10n.watchPreferences,
onTap: () => onOpen(MineDestination.preferences),
),
],
),
),
TableRow(
children: [
_cell(
index: 4,
actionKey: const Key('mine-help'),
icon: Icons.support_agent_rounded,
iconColor: AppColors.mineIconSupport,
label: l10n.contactSupport,
onTap: () => onOpen(MineDestination.help),
if (value != null && value.isNotEmpty) ...[
const SizedBox(width: AppDimens.spaceSm),
Text(
value,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.mineServiceValueFont,
),
_cell(
index: 5,
actionKey: const Key('mine-language'),
icon: Icons.language_rounded,
iconColor: AppColors.mineIconLanguage,
label: l10n.language,
onTap: onLanguage,
),
_cell(
index: 6,
actionKey: const Key('mine-settings'),
icon: Icons.settings_outlined,
iconColor: AppColors.mineIconSettings,
label: l10n.settings,
onTap: () => onOpen(MineDestination.settings),
),
_cell(
index: 7,
actionKey: const Key('mine-notices'),
icon: Icons.notifications_none_rounded,
iconColor: AppColors.mineIconNotices,
label: l10n.notices,
onTap: () => onOpen(MineDestination.notices),
),
],
),
],
const SizedBox(width: AppDimens.spaceXs),
const Icon(
Icons.chevron_right_rounded,
color: AppColors.textSecondary,
size: 22,
),
],
),
),
);
}
Widget _cell({
required int index,
Key? actionKey,
IconData? icon,
Color? iconColor,
String? label,
VoidCallback? onTap,
}) {
return SizedBox(
key: Key('mine-menu-cell-$index'),
height: AppDimens.mineGridCellHeight,
child: actionKey == null
? const SizedBox.shrink()
: InkWell(
key: actionKey,
onTap: onTap,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: AppDimens.mineGridIconSurface,
height: AppDimens.mineGridIconSurface,
decoration: const BoxDecoration(
color: AppColors.mineGridIconSurface,
shape: BoxShape.circle,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: iconColor?.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: AppDimens.mineGridIcon,
),
),
),
const SizedBox(height: AppDimens.spaceXs + 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
label ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.mineGridLabel,
fontSize: AppDimens.mineGridLabel,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
),
],
),
),
),
);
}
}
@@ -1,93 +0,0 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
class MineMoreServices extends StatelessWidget {
const MineMoreServices({
super.key,
required this.onCommonQuestions,
required this.onAbout,
});
final VoidCallback onCommonQuestions;
final VoidCallback onAbout;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
key: const Key('mine-more-services'),
decoration: BoxDecoration(
color: AppColors.minePanelSurface,
borderRadius: BorderRadius.circular(AppDimens.mineGridRadius),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_row(
key: const Key('mine-common-questions'),
label: l10n.commonQuestions,
onTap: onCommonQuestions,
),
const Divider(
height: AppDimens.mineGridDividerWidth,
thickness: AppDimens.mineGridDividerWidth,
color: AppColors.mineMoreDivider,
),
_row(
key: const Key('mine-about'),
label: l10n.aboutUs,
onTap: onAbout,
),
],
),
);
}
Widget _row({
required Key key,
required String label,
required VoidCallback onTap,
}) {
return Material(
color: AppColors.transparent,
child: InkWell(
key: key,
onTap: onTap,
child: SizedBox(
height: AppDimens.mineMoreRowHeight,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.minePanelPadding,
),
child: Row(
children: [
Expanded(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.mineMoreFont,
fontWeight: FontWeight.w400,
),
),
),
const Icon(
Icons.chevron_right_rounded,
color: AppColors.textSecondary,
size: 18,
),
],
),
),
),
),
);
}
}