feat(frontend): add dark mode support and fix mining records overflow

- Add darkTheme configuration in main.dart with themeModeProvider
- Refactor profile_page.dart to use Theme.of(context).colorScheme for dynamic theming
- Fix mining_records_page.dart layout overflow by using Expanded/Flexible widgets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-19 05:06:28 -08:00
parent 75e74b07c3
commit 8619b0bf26
3 changed files with 115 additions and 75 deletions

View File

@ -7,6 +7,7 @@ import 'core/constants/app_colors.dart';
import 'core/network/api_client.dart'; import 'core/network/api_client.dart';
import 'core/router/routes.dart'; import 'core/router/routes.dart';
import 'presentation/providers/user_providers.dart'; import 'presentation/providers/user_providers.dart';
import 'presentation/providers/settings_providers.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -46,10 +47,12 @@ class _MiningAppState extends ConsumerState<MiningApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final router = ref.watch(appRouterProvider); final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router( return MaterialApp.router(
title: '股行', title: '股行',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
themeMode: themeMode,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary, seedColor: AppColors.primary,
@ -67,6 +70,23 @@ class _MiningAppState extends ConsumerState<MiningApp> {
), ),
), ),
), ),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.dark,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: const CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
routerConfig: router, routerConfig: router,
); );
} }

View File

@ -239,9 +239,13 @@ class _MiningRecordsListPageState extends ConsumerState<MiningRecordsListPage> {
// + // +
Row( Row(
children: [ children: [
_buildInfoItem('贡献值占比', _formatPercent(record.contributionRatio)), Expanded(
const SizedBox(width: 24), child: _buildInfoItem('贡献值占比', _formatPercent(record.contributionRatio)),
_buildInfoItem('价格快照', _formatPrice(record.priceSnapshot)), ),
const SizedBox(width: 16),
Expanded(
child: _buildInfoItem('价格快照', _formatPrice(record.priceSnapshot)),
),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -270,13 +274,16 @@ class _MiningRecordsListPageState extends ConsumerState<MiningRecordsListPage> {
'$label: ', '$label: ',
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)), style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)),
), ),
Text( Flexible(
value, child: Text(
style: const TextStyle( value,
fontSize: 12, style: const TextStyle(
color: _darkText, fontSize: 12,
fontWeight: FontWeight.w500, color: _darkText,
fontFamily: 'monospace', fontWeight: FontWeight.w500,
fontFamily: 'monospace',
),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],

View File

@ -10,13 +10,9 @@ import '../../widgets/shimmer_loading.dart';
class ProfilePage extends ConsumerWidget { class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
// //
static const Color _orange = Color(0xFFFF6B00); static const Color _orange = Color(0xFFFF6B00);
static const Color _green = Color(0xFF10B981); static const Color _green = Color(0xFF10B981);
static const Color _darkText = Color(0xFF1F2937);
static const Color _grayText = Color(0xFF6B7280);
static const Color _lightGray = Color(0xFF9CA3AF);
static const Color _bgGray = Color(0xFFF3F4F6);
static const Color _red = Color(0xFFEF4444); static const Color _red = Color(0xFFEF4444);
// //
@ -35,12 +31,13 @@ class ProfilePage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userNotifierProvider); final user = ref.watch(userNotifierProvider);
final statsAsync = ref.watch(userStatsProvider); final statsAsync = ref.watch(userStatsProvider);
final colorScheme = Theme.of(context).colorScheme;
final isStatsLoading = statsAsync.isLoading; final isStatsLoading = statsAsync.isLoading;
final stats = statsAsync.valueOrNull; final stats = statsAsync.valueOrNull;
return Scaffold( return Scaffold(
backgroundColor: _bgGray, backgroundColor: colorScheme.surfaceContainerHighest,
body: SafeArea( body: SafeArea(
bottom: false, bottom: false,
child: RefreshIndicator( child: RefreshIndicator(
@ -53,46 +50,46 @@ class ProfilePage extends ConsumerWidget {
child: Column( child: Column(
children: [ children: [
// //
_buildUserHeader(context, user), _buildUserHeader(context, user, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildStatsRow(stats, isStatsLoading), _buildStatsRow(context, stats, isStatsLoading, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildAccountSettings(context, user), _buildAccountSettings(context, user, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildRecordsSection(context), _buildRecordsSection(context, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildTeamEarningsSection(context), _buildTeamEarningsSection(context, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildOtherSettings(context, ref), _buildOtherSettings(context, ref, colorScheme),
const SizedBox(height: 24), const SizedBox(height: 24),
// 退 // 退
_buildLogoutButton(context, ref), _buildLogoutButton(context, ref, colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
const Text( Text(
'Version 1.0.0', 'Version 1.0.0',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _lightGray, color: colorScheme.outline,
), ),
), ),
@ -105,12 +102,12 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildUserHeader(BuildContext context, UserState user) { Widget _buildUserHeader(BuildContext context, UserState user, ColorScheme colorScheme) {
final avatarColor = _avatarColors[user.avatarIndex % _avatarColors.length]; final avatarColor = _avatarColors[user.avatarIndex % _avatarColors.length];
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
color: Colors.white, color: colorScheme.surface,
child: Row( child: Row(
children: [ children: [
// //
@ -157,10 +154,10 @@ class ProfilePage extends ConsumerWidget {
user.nickname?.isNotEmpty == true user.nickname?.isNotEmpty == true
? user.nickname! ? user.nickname!
: (user.realName ?? '股行用户'), : (user.realName ?? '股行用户'),
style: const TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -198,9 +195,9 @@ class ProfilePage extends ConsumerWidget {
if (user.phone != null) if (user.phone != null)
Text( Text(
'ID: ${user.phone}', 'ID: ${user.phone}',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: _grayText, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@ -210,9 +207,9 @@ class ProfilePage extends ConsumerWidget {
// //
IconButton( IconButton(
onPressed: () => context.push(Routes.editProfile), onPressed: () => context.push(Routes.editProfile),
icon: const Icon( icon: Icon(
Icons.edit_outlined, Icons.edit_outlined,
color: _grayText, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@ -220,10 +217,10 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildStatsRow(UserStats? stats, bool isLoading) { Widget _buildStatsRow(BuildContext context, UserStats? stats, bool isLoading, ColorScheme colorScheme) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
color: Colors.white, color: colorScheme.surface,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@ -231,81 +228,85 @@ class ProfilePage extends ConsumerWidget {
'参与状态', '参与状态',
stats?.hasAdopted == true ? '已参与' : '未参与', stats?.hasAdopted == true ? '已参与' : '未参与',
isLoading, isLoading,
colorScheme,
), ),
_buildDivider(), _buildDivider(colorScheme),
_buildStatItem( _buildStatItem(
'引荐人数', '引荐人数',
stats?.directReferralAdoptedCount.toString() ?? '0', stats?.directReferralAdoptedCount.toString() ?? '0',
isLoading, isLoading,
colorScheme,
), ),
_buildDivider(), _buildDivider(colorScheme),
_buildStatItem( _buildStatItem(
'团队下贡献值', '团队下贡献值',
stats?.unlockedLevelDepth.toString() ?? '0', stats?.unlockedLevelDepth.toString() ?? '0',
isLoading, isLoading,
colorScheme,
), ),
_buildDivider(), _buildDivider(colorScheme),
_buildStatItem( _buildStatItem(
'团队上贡献值', '团队上贡献值',
'15', '15',
isLoading, isLoading,
colorScheme,
), ),
], ],
), ),
); );
} }
Widget _buildStatItem(String label, String value, bool isLoading) { Widget _buildStatItem(String label, String value, bool isLoading, ColorScheme colorScheme) {
return Column( return Column(
children: [ children: [
DataText( DataText(
data: isLoading ? null : value, data: isLoading ? null : value,
isLoading: isLoading, isLoading: isLoading,
placeholder: '--', placeholder: '--',
style: const TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _grayText, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
); );
} }
Widget _buildDivider() { Widget _buildDivider(ColorScheme colorScheme) {
return Container( return Container(
width: 1, width: 1,
height: 30, height: 30,
color: _bgGray, color: colorScheme.outlineVariant,
); );
} }
Widget _buildAccountSettings(BuildContext context, UserState user) { Widget _buildAccountSettings(BuildContext context, UserState user, ColorScheme colorScheme) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8), padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text( child: Text(
'账户设置', '账户设置',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
), ),
@ -314,6 +315,7 @@ class ProfilePage extends ConsumerWidget {
label: '账户安全', label: '账户安全',
onTap: () => context.push(Routes.changePassword), onTap: () => context.push(Routes.changePassword),
showDivider: false, showDivider: false,
colorScheme: colorScheme,
), ),
], ],
), ),
@ -326,16 +328,17 @@ class ProfilePage extends ConsumerWidget {
Widget? trailing, Widget? trailing,
required VoidCallback onTap, required VoidCallback onTap,
bool showDivider = true, bool showDivider = true,
required ColorScheme colorScheme,
}) { }) {
return Column( return Column(
children: [ children: [
ListTile( ListTile(
leading: Icon(icon, color: _grayText, size: 22), leading: Icon(icon, color: colorScheme.onSurfaceVariant, size: 22),
title: Text( title: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
trailing: Row( trailing: Row(
@ -343,7 +346,7 @@ class ProfilePage extends ConsumerWidget {
children: [ children: [
if (trailing != null) trailing, if (trailing != null) trailing,
if (trailing != null) const SizedBox(width: 8), if (trailing != null) const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: _lightGray, size: 20), Icon(Icons.chevron_right, color: colorScheme.outline, size: 20),
], ],
), ),
onTap: onTap, onTap: onTap,
@ -354,23 +357,23 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildRecordsSection(BuildContext context) { Widget _buildRecordsSection(BuildContext context, ColorScheme colorScheme) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'我的记录', '我的记录',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -381,16 +384,19 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.eco, icon: Icons.eco,
label: '参与记录', label: '参与记录',
onTap: () => context.push(Routes.plantingRecords), onTap: () => context.push(Routes.plantingRecords),
colorScheme: colorScheme,
), ),
_buildRecordIcon( _buildRecordIcon(
icon: Icons.assignment, icon: Icons.assignment,
label: '分配记录', label: '分配记录',
onTap: () => context.push(Routes.miningRecords), onTap: () => context.push(Routes.miningRecords),
colorScheme: colorScheme,
), ),
_buildRecordIcon( _buildRecordIcon(
icon: Icons.receipt_long, icon: Icons.receipt_long,
label: '交易记录', label: '交易记录',
onTap: () => context.push(Routes.tradingRecords), onTap: () => context.push(Routes.tradingRecords),
colorScheme: colorScheme,
), ),
], ],
), ),
@ -403,6 +409,7 @@ class ProfilePage extends ConsumerWidget {
required IconData icon, required IconData icon,
required String label, required String label,
required VoidCallback onTap, required VoidCallback onTap,
required ColorScheme colorScheme,
}) { }) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@ -421,9 +428,9 @@ class ProfilePage extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _grayText, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@ -431,24 +438,24 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildTeamEarningsSection(BuildContext context) { Widget _buildTeamEarningsSection(BuildContext context, ColorScheme colorScheme) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8), padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text( child: Text(
'团队与收益', '团队与收益',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
), ),
@ -457,33 +464,34 @@ class ProfilePage extends ConsumerWidget {
label: '我的团队', label: '我的团队',
onTap: () => context.push(Routes.myTeam), onTap: () => context.push(Routes.myTeam),
showDivider: false, showDivider: false,
colorScheme: colorScheme,
), ),
], ],
), ),
); );
} }
Widget _buildOtherSettings(BuildContext context, WidgetRef ref) { Widget _buildOtherSettings(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
final notificationsEnabled = ref.watch(notificationsEnabledProvider); final notificationsEnabled = ref.watch(notificationsEnabledProvider);
final darkModeEnabled = ref.watch(darkModeEnabledProvider); final darkModeEnabled = ref.watch(darkModeEnabledProvider);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8), padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text( child: Text(
'其他设置', '其他设置',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
), ),
@ -494,6 +502,7 @@ class ProfilePage extends ConsumerWidget {
onChanged: (value) { onChanged: (value) {
ref.read(notificationsEnabledProvider.notifier).setEnabled(value); ref.read(notificationsEnabledProvider.notifier).setEnabled(value);
}, },
colorScheme: colorScheme,
), ),
_buildSwitchItem( _buildSwitchItem(
icon: Icons.dark_mode_outlined, icon: Icons.dark_mode_outlined,
@ -502,17 +511,20 @@ class ProfilePage extends ConsumerWidget {
onChanged: (value) { onChanged: (value) {
ref.read(darkModeEnabledProvider.notifier).setEnabled(value); ref.read(darkModeEnabledProvider.notifier).setEnabled(value);
}, },
colorScheme: colorScheme,
), ),
_buildSettingItem( _buildSettingItem(
icon: Icons.help_outline, icon: Icons.help_outline,
label: '帮助中心', label: '帮助中心',
onTap: () => context.push(Routes.helpCenter), onTap: () => context.push(Routes.helpCenter),
colorScheme: colorScheme,
), ),
_buildSettingItem( _buildSettingItem(
icon: Icons.info_outline, icon: Icons.info_outline,
label: '关于我们', label: '关于我们',
onTap: () => context.push(Routes.about), onTap: () => context.push(Routes.about),
showDivider: false, showDivider: false,
colorScheme: colorScheme,
), ),
], ],
), ),
@ -524,16 +536,17 @@ class ProfilePage extends ConsumerWidget {
required String label, required String label,
required bool value, required bool value,
required ValueChanged<bool> onChanged, required ValueChanged<bool> onChanged,
required ColorScheme colorScheme,
}) { }) {
return Column( return Column(
children: [ children: [
ListTile( ListTile(
leading: Icon(icon, color: _grayText, size: 22), leading: Icon(icon, color: colorScheme.onSurfaceVariant, size: 22),
title: Text( title: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: _darkText, color: colorScheme.onSurface,
), ),
), ),
trailing: Switch( trailing: Switch(
@ -548,7 +561,7 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildLogoutButton(BuildContext context, WidgetRef ref) { Widget _buildLogoutButton(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox( child: SizedBox(
@ -580,7 +593,7 @@ class ProfilePage extends ConsumerWidget {
); );
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: colorScheme.surface,
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),