diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0f6bdad..14da421 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,7 +57,7 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - screen_brightness_ios: 7437207a2a9bc56553aa10f782afecf830b4c4e2 + screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe diff --git a/lib/main.dart b/lib/main.dart index f79d5b1..9eb9191 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'providers/reservation_provider.dart'; import 'providers/theme_provider.dart'; // 新增 import 'providers/ride_history_provider.dart'; // 新增 import 'providers/brightness_provider.dart'; +import 'providers/visualization_settings_provider.dart'; import 'screens/login/login_page.dart'; import 'screens/main/main_page.dart'; import 'package:flutter/services.dart'; @@ -29,6 +30,9 @@ void main() { ), update: (context, auth, previous) => RideHistoryProvider(auth), ), + ChangeNotifierProvider( + create: (_) => VisualizationSettingsProvider(), + ), ], child: MyApp(), ), diff --git a/lib/providers/visualization_settings_provider.dart b/lib/providers/visualization_settings_provider.dart new file mode 100644 index 0000000..2b9e38b --- /dev/null +++ b/lib/providers/visualization_settings_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum TimeRange { threeMonths, sixMonths, oneYear, all } + +class VisualizationSettingsProvider with ChangeNotifier { + TimeRange _selectedTimeRange = TimeRange.all; + static const String _timeRangeKey = 'selected_time_range'; + + TimeRange get selectedTimeRange => _selectedTimeRange; + + VisualizationSettingsProvider() { + _loadSettings(); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + final savedRange = prefs.getString(_timeRangeKey); + if (savedRange != null) { + _selectedTimeRange = TimeRange.values.firstWhere( + (e) => e.toString() == savedRange, + orElse: () => TimeRange.all, + ); + notifyListeners(); + } + } + + Future setTimeRange(TimeRange range) async { + if (_selectedTimeRange != range) { + _selectedTimeRange = range; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_timeRangeKey, range.toString()); + notifyListeners(); + } + } +} diff --git a/lib/screens/login/login_page.dart b/lib/screens/login/login_page.dart index b5996f2..5dac413 100644 --- a/lib/screens/login/login_page.dart +++ b/lib/screens/login/login_page.dart @@ -35,7 +35,7 @@ class LoginPageState extends State { title: Text('用户须知'), content: SingleChildScrollView( child: Text( - '我们将采集以SHA256加密后的用户名、版本号及设备类型,用于统计每日活跃用户数您的用户名和密码将始终安全保存在您的设备上,不会上传至服务器。', + '我们将采集以SHA256加密后的用户名、版本号及设备类型,用于统计每日活跃用户数。您的用户名和密码将始终安全保存在您的设备上,不会上传至服务器。', ), ), actions: [ @@ -58,14 +58,14 @@ class LoginPageState extends State { body: Center( child: SingleChildScrollView( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 400), // 添加最大宽度约束 + constraints: BoxConstraints(maxWidth: 400), child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), child: Form( key: _formKey, child: Column( - mainAxisAlignment: MainAxisAlignment.center, // 垂直居中 - crossAxisAlignment: CrossAxisAlignment.stretch, // 水平拉伸 + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Marchkov Helper', @@ -167,7 +167,6 @@ class AnimatedTermsCheckbox extends StatelessWidget { final VoidCallback onTermsTap; const AnimatedTermsCheckbox({ - super.key, // 修改这里 required this.value, required this.onChanged, required this.onTermsTap, diff --git a/lib/screens/main/main_page.dart b/lib/screens/main/main_page.dart index 80f8ed3..3573e5f 100644 --- a/lib/screens/main/main_page.dart +++ b/lib/screens/main/main_page.dart @@ -4,6 +4,7 @@ import '../ride/ride_page.dart'; import '../settings/settings_page.dart'; import '../reservation/reservation_page.dart'; import 'package:flutter/services.dart'; // 新增导入 +import 'package:shared_preferences/shared_preferences.dart'; // 新增 class MainPage extends StatefulWidget { @override @@ -13,6 +14,26 @@ class MainPage extends StatefulWidget { class MainPageState extends State { int _selectedIndex = 0; + @override + void initState() { + super.initState(); + _loadSelectedIndex(); + } + + // 加载保存的页面索引 + Future _loadSelectedIndex() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedIndex = prefs.getInt('selectedMainPageIndex') ?? 0; + }); + } + + // 保存当前页面索引 + Future _saveSelectedIndex(int index) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('selectedMainPageIndex', index); + } + static List _widgetOptions = [ RidePage(), ReservationPage(), @@ -24,6 +45,7 @@ class MainPageState extends State { setState(() { _selectedIndex = index; }); + _saveSelectedIndex(index); // 保存选中的索引 } @override diff --git a/lib/screens/reservation/bus_list.dart b/lib/screens/reservation/bus_list.dart index 0a53803..01e584f 100644 --- a/lib/screens/reservation/bus_list.dart +++ b/lib/screens/reservation/bus_list.dart @@ -7,6 +7,7 @@ class BusList extends StatelessWidget { final Function(Map) showBusDetails; final Map reservedBuses; final Map buttonCooldowns; + final bool isRefreshing; const BusList({ super.key, @@ -15,28 +16,73 @@ class BusList extends StatelessWidget { required this.showBusDetails, required this.reservedBuses, required this.buttonCooldowns, + this.isRefreshing = false, }); @override Widget build(BuildContext context) { - return ListView( - padding: EdgeInsets.only(top: 8), + final theme = Theme.of(context); + + return Stack( children: [ - BusSection( - title: '去燕园', - buses: _getBusesByDirection('去燕园'), - onBusCardTap: onBusCardTap, - showBusDetails: showBusDetails, - reservedBuses: reservedBuses, - buttonCooldowns: buttonCooldowns, // 传递 Map + ListView( + padding: EdgeInsets.only(top: 24), + children: [ + BusSection( + title: '去燕园', + buses: _getBusesByDirection('去燕园'), + onBusCardTap: onBusCardTap, + showBusDetails: showBusDetails, + reservedBuses: reservedBuses, + buttonCooldowns: buttonCooldowns, + ), + BusSection( + title: '去昌平', + buses: _getBusesByDirection('去昌平'), + onBusCardTap: onBusCardTap, + showBusDetails: showBusDetails, + reservedBuses: reservedBuses, + buttonCooldowns: buttonCooldowns, + ), + ], ), - BusSection( - title: '去昌平', - buses: _getBusesByDirection('去昌平'), - onBusCardTap: onBusCardTap, - showBusDetails: showBusDetails, - reservedBuses: reservedBuses, - buttonCooldowns: buttonCooldowns, // 传递 Map + Positioned( + top: 12, + left: 16, + right: 16, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: isRefreshing + ? Container( + height: 2, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + theme.colorScheme.primary.withOpacity(0), + theme.colorScheme.primary.withOpacity(0.5), + theme.colorScheme.primary, + theme.colorScheme.primary.withOpacity(0.5), + theme.colorScheme.primary.withOpacity(0), + ], + stops: [0.0, 0.25, 0.5, 0.75, 1.0], + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(1), + child: LinearProgressIndicator( + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + theme.colorScheme.primary.withOpacity(0.3), + ), + minHeight: 2, + ), + ), + ) + : SizedBox(height: 2), + ), ), ], ); diff --git a/lib/screens/reservation/reservation_page.dart b/lib/screens/reservation/reservation_page.dart index efed99d..bd3d535 100644 --- a/lib/screens/reservation/reservation_page.dart +++ b/lib/screens/reservation/reservation_page.dart @@ -23,17 +23,22 @@ class _ReservationPageState extends State { List _busList = []; List _filteredBusList = []; bool _isLoading = true; - String _errorMessage = ''; Map _reservedBuses = {}; late DauService _dauService; bool? _showTip; Map _buttonCooldowns = {}; + bool _isBackgroundRefreshing = false; + bool _showRetryButton = false; + bool _isRetrying = false; + DateTime? _lastRetryTime; + String _loadingStep = '正在初始化...'; @override void initState() { super.initState(); _selectedDay = _focusedDay; _loadReservationData(); + _startSlowLoadingTimer(); final authProvider = Provider.of(context, listen: false); final versionService = VersionService(); @@ -41,12 +46,34 @@ class _ReservationPageState extends State { _dauService.sendDailyActive(); } + void _startSlowLoadingTimer() { + Future.delayed(Duration(seconds: 2), () { + if (mounted && _isLoading) { + setState(() { + _showRetryButton = true; + _loadingStep = '加载缓慢,可能是校园网连接较弱'; + }); + } + }); + } + Future _loadReservationData() async { + if (mounted) { + setState(() { + _loadingStep = '正在检查缓存数据...'; + }); + } + final prefs = await SharedPreferences.getInstance(); final cachedDate = prefs.getString('cachedDate'); final todayString = DateTime.now().toIso8601String().split('T')[0]; if (cachedDate == todayString) { + if (mounted) { + setState(() { + _loadingStep = '正在加载缓存数据...'; + }); + } final cachedBusDataString = prefs.getString('cachedBusData'); if (cachedBusDataString != null) { final cachedBusData = jsonDecode(cachedBusDataString); @@ -63,6 +90,7 @@ class _ReservationPageState extends State { _busList = cachedBusData; _filterBusList(); _isLoading = false; + _isBackgroundRefreshing = true; }); } } else { @@ -93,6 +121,8 @@ class _ReservationPageState extends State { _updateReservedBusesWithRecentReservations(recentReservations); _filterBusList(); _isLoading = false; + _isBackgroundRefreshing = false; + _loadingStep = '加载完成'; }); await _cacheBusData(); @@ -100,8 +130,10 @@ class _ReservationPageState extends State { } catch (e) { if (!mounted) return; setState(() { - _errorMessage = '加载失败: $e'; - _isLoading = false; + _loadingStep = '加载失败: ${e.toString()}'; + _isLoading = true; + _showRetryButton = true; + _isBackgroundRefreshing = false; }); } } @@ -271,6 +303,36 @@ class _ReservationPageState extends State { final authProvider = Provider.of(context, listen: false); final reservationService = ReservationService(authProvider); + // 设置加载状态 + setState(() { + _isBackgroundRefreshing = true; + }); + + // 创建一个计时器,8秒后检查是否仍在加载 + Timer? timeoutTimer; + timeoutTimer = Timer(Duration(seconds: 8), () { + if (mounted && _isBackgroundRefreshing) { + setState(() { + _isBackgroundRefreshing = false; + _showRetryButton = true; + _loadingStep = '加载缓慢,可能是校园网连接较弱'; + }); + + // 显示一个 SnackBar 提示用户 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('加载缓慢,可能是校园网连接较弱'), + action: SnackBarAction( + label: '重试', + onPressed: _retryWithLogin, + ), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 4), + ), + ); + } + }); + try { final today = DateTime.now(); final dateStrings = [ @@ -282,18 +344,105 @@ class _ReservationPageState extends State { final recentReservations = await reservationService.fetchRecentReservations(); + // 取消计时器 + timeoutTimer.cancel(); + if (!mounted) return; setState(() { _busList = allBuses; _updateReservedBusesWithRecentReservations(recentReservations); _filterBusList(); + _isBackgroundRefreshing = false; + _showRetryButton = false; }); await _cacheBusData(); await _cacheReservedBuses(); } catch (e) { - print('刷新班车数据失败: $e'); - // 可以选择在这里显示一个短暂的错误提示 + // 取消计时器 + timeoutTimer.cancel(); + + if (!mounted) return; + setState(() { + _isBackgroundRefreshing = false; + _showRetryButton = true; + }); + + // 显示错误提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('加载失败: ${e.toString()}'), + action: SnackBarAction( + label: '重试', + onPressed: _retryWithLogin, + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + Future _retryWithLogin() async { + final now = DateTime.now(); + if (_lastRetryTime != null && + now.difference(_lastRetryTime!) < Duration(seconds: 3)) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('请稍后再试'), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 2), + ), + ); + return; + } + _lastRetryTime = now; + + if (_isRetrying) return; + + setState(() { + _isRetrying = true; + _loadingStep = '正在刷新登录状态...'; + }); + + try { + final prefs = await SharedPreferences.getInstance(); + final savedUsername = prefs.getString('username'); + final savedPassword = prefs.getString('password'); + + if (savedUsername != null && savedPassword != null) { + if (!mounted) return; + final authProvider = Provider.of(context, listen: false); + await authProvider.login(savedUsername, savedPassword); + + setState(() { + _isLoading = true; + _showRetryButton = false; + _loadingStep = '正在重新加载数据...'; + }); + + await _loadReservationData(); + } else { + throw Exception('未找到登录凭据,请重新登录'); + } + } catch (e) { + if (mounted) { + setState(() { + _loadingStep = '重试失败: ${e.toString()}'; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('重试失败: ${e.toString()}'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isRetrying = false; + }); + } } } @@ -375,43 +524,47 @@ class _ReservationPageState extends State { color: theme.colorScheme.primary, child: _isLoading ? Center( - child: CircularProgressIndicator( - color: theme.colorScheme.primary, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isRetrying) + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + _loadingStep, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + if (_showRetryButton && !_isRetrying) ...[ + SizedBox(height: 16), + ElevatedButton( + onPressed: _retryWithLogin, + style: ElevatedButton.styleFrom( + minimumSize: Size(120, 36), + ), + child: Text('重试'), + ), + ], + ], ), ) - : _errorMessage.isNotEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 48, - color: theme.colorScheme.error, - ), - SizedBox(height: 16), - Text( - _errorMessage, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.error, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 24), - FilledButton.tonal( - onPressed: _refreshBusData, - child: Text('重试'), - ), - ], - ), - ) - : BusList( - filteredBusList: _filteredBusList, - onBusCardTap: _onBusCardTap, - showBusDetails: _showBusDetails, - reservedBuses: _reservedBuses, - buttonCooldowns: _buttonCooldowns, - ), + : BusList( + filteredBusList: _filteredBusList, + onBusCardTap: _onBusCardTap, + showBusDetails: _showBusDetails, + reservedBuses: _reservedBuses, + buttonCooldowns: _buttonCooldowns, + isRefreshing: _isBackgroundRefreshing, + ), ), ), ], diff --git a/lib/screens/settings/help_page.dart b/lib/screens/settings/help_page.dart index c910be1..d45806c 100644 --- a/lib/screens/settings/help_page.dart +++ b/lib/screens/settings/help_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class HelpPage extends StatelessWidget { const HelpPage({super.key}); @@ -34,7 +35,7 @@ class HelpPage extends StatelessWidget { icon: Icons.directions_bus_outlined, ), _HelpItem( - title: '仿官方页面', + title: '易验丁真', content: '点击二维码可以切换到仿官方页面。主页面和仿官方页面的二维码都是有效的。该功能默认关闭,您可以在设置中开启此功能。', icon: Icons.qr_code, @@ -60,7 +61,19 @@ class HelpPage extends StatelessWidget { _HelpItem( title: '自动预约', content: '应用会自动为您预约最合适的班车,直接出码,无需操作。该功能默认关闭,您可以在设置中开启此功能。', - icon: Icons.schedule, + icon: Icons.auto_mode, + ), + _HelpItem( + title: '亮度调节', + content: + '应用支持自动调节二维码页面的亮度。开启后,显示二维码时会自动提高屏幕亮度,便于扫码;离开二维码页面后会自动恢复原有亮度。', + icon: Icons.brightness_auto, + ), + _HelpItem( + title: '预约历史', + content: + '您可以在设置中查看预约历史的可视化统计图表,了解自己的乘车习惯。图表展示了您在不同时段的乘车频率,以及常用的班车路线。', + icon: Icons.bar_chart, ), ], ), @@ -75,6 +88,19 @@ class HelpPage extends StatelessWidget { '如果加载太慢,请尝试关闭代理或连接校园网。这种情况在校园网连接差的情况下非常常见。如果问题仍然存在,可以尝试退出登录重新进入。', icon: Icons.speed, ), + _HelpItem( + title: '用户反馈', + content: + '如果您有任何建议或者遇到问题,欢迎发送邮件至开发者邮箱,或请求加入用户交流群。\n\n点击复制邮箱:shuttle@variantconst.com', + icon: Icons.feedback_outlined, + onContentTap: () { + const email = 'shuttle@variantconst.com'; + Clipboard.setData(ClipboardData(text: email)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('邮箱已复制到剪贴板')), + ); + }, + ), ], ), ], @@ -144,12 +170,15 @@ class HelpPage extends StatelessWidget { ), ), children: [ - Padding( - padding: EdgeInsets.fromLTRB(56, 0, 16, 16), - child: Text( - item.content, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + InkWell( + onTap: item.onContentTap, + child: Padding( + padding: EdgeInsets.fromLTRB(56, 0, 16, 16), + child: Text( + item.content, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), ), ), ), @@ -165,10 +194,12 @@ class _HelpItem { final String title; final String content; final IconData icon; + final VoidCallback? onContentTap; const _HelpItem({ required this.title, required this.content, required this.icon, + this.onContentTap, }); } diff --git a/lib/screens/settings/ride_settings_page.dart b/lib/screens/settings/ride_settings_page.dart index 366bf5b..6714374 100644 --- a/lib/screens/settings/ride_settings_page.dart +++ b/lib/screens/settings/ride_settings_page.dart @@ -153,6 +153,11 @@ class RideSettingsPageState extends State _dayBrightness = 75.0; _nightBrightness = 50.0; }); + + // 由于默认是 auto 模式,需要展开亮度调节部分 + if (_animationController != null) { + _animationController!.forward(); + } } void _showResetConfirmationDialog() { @@ -289,8 +294,6 @@ class RideSettingsPageState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - backgroundColor: theme.colorScheme.errorContainer, - foregroundColor: theme.colorScheme.onErrorContainer, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/screens/settings/settings_page.dart b/lib/screens/settings/settings_page.dart index b188d2d..c3a453c 100644 --- a/lib/screens/settings/settings_page.dart +++ b/lib/screens/settings/settings_page.dart @@ -388,6 +388,7 @@ class _SettingsPageState extends State { await prefs.remove('selectedEmoji'); await prefs.remove('cachedRideHistory'); await prefs.remove('lastDauSentDate'); + await prefs.remove('selected_time_range'); // await prefs.remove('autoReservationEnabled'); // await prefs.remove('safariStyleEnabled'); await prefs.remove('isBrightnessEnhanced'); diff --git a/lib/screens/settings/theme_settings_page.dart b/lib/screens/settings/theme_settings_page.dart index 5bd3ef7..88c817c 100644 --- a/lib/screens/settings/theme_settings_page.dart +++ b/lib/screens/settings/theme_settings_page.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../../providers/theme_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ThemeSettingsPage extends StatelessWidget { @override @@ -11,6 +12,15 @@ class ThemeSettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('selectedMainPageIndex', 2); + // ignore: use_build_context_synchronously + Navigator.pop(context); + }, + ), title: Text( '主题设置', style: Theme.of(context).textTheme.titleLarge, diff --git a/lib/screens/visualization/ride_calendar_card.dart b/lib/screens/visualization/ride_calendar_card.dart index 354ba1f..eedb985 100644 --- a/lib/screens/visualization/ride_calendar_card.dart +++ b/lib/screens/visualization/ride_calendar_card.dart @@ -104,6 +104,8 @@ class RideCalendarCardState extends State { Icons.chevron_right, color: theme.colorScheme.primary, ), + titleCentered: true, + headerPadding: EdgeInsets.symmetric(horizontal: 16.0), ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: TextStyle( @@ -240,16 +242,18 @@ class RideCalendarCardState extends State { } return ListView.separated( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 8, + ), itemCount: selectedEvents.length, - separatorBuilder: (context, index) => SizedBox(height: 12), + separatorBuilder: (context, index) => SizedBox(height: 8), itemBuilder: (context, index) { RideInfo ride = selectedEvents[index]; - String direction = ride.resourceName.contains('燕园') ? '去燕园' : '去昌平'; - - // 修改状态判断逻辑 String status = ride.statusName; - bool isViolation = status == '已预约'; // 将"已预约"视为违约 + bool isViolation = status == '已预约'; String statusText = isViolation ? '已违约' : '已签到'; return Container( @@ -257,112 +261,51 @@ class RideCalendarCardState extends State { borderRadius: BorderRadius.circular(12), color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3), ), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 左侧状态指示条 - Container( - width: 4, - margin: EdgeInsets.symmetric(vertical: 2), - decoration: BoxDecoration( - color: isViolation - ? theme.colorScheme.error - : theme.colorScheme.primary, - borderRadius: BorderRadius.circular(2), - ), - ), - Expanded( - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 头部信息 - Row( - children: [ - Icon( - isViolation - ? Icons.error_outline - : Icons.directions_bus, - size: 20, - color: isViolation - ? theme.colorScheme.error - : theme.colorScheme.primary, - ), - SizedBox(width: 8), - Text( - direction, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w500, - color: theme.colorScheme.onSurface, - ), - ), - Spacer(), - Container( - padding: EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: isViolation - ? theme.colorScheme.errorContainer - : theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - statusText, // 使用计算后的状态文本 - style: theme.textTheme.labelSmall?.copyWith( - color: isViolation - ? theme.colorScheme.onErrorContainer - : theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - SizedBox(height: 12), - // 时间和地点信息 - Row( - children: [ - Icon( - Icons.access_time, - size: 16, - color: theme.colorScheme.onSurfaceVariant, - ), - SizedBox(width: 4), - Expanded( - child: Text( - ride.appointmentTime, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - ), - ), - ], - ), - SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.location_on, - size: 16, - color: theme.colorScheme.onSurfaceVariant, - ), - SizedBox(width: 4), - Expanded( - child: Text( - ride.resourceName, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], + // 第一行:时间和状态 + Row( + children: [ + Text( + ride.appointmentTime, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + Spacer(), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: isViolation + ? theme.colorScheme.errorContainer + : theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + statusText, + style: theme.textTheme.labelSmall?.copyWith( + color: isViolation + ? theme.colorScheme.onErrorContainer + : theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w500, ), - ], + ), ), + ], + ), + // 第二行:路线名 + SizedBox(height: 4), + Text( + ride.resourceName, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, ), ), ], diff --git a/lib/screens/visualization/ride_heatmap.dart b/lib/screens/visualization/ride_heatmap.dart index 099c2b7..d91ff5c 100644 --- a/lib/screens/visualization/ride_heatmap.dart +++ b/lib/screens/visualization/ride_heatmap.dart @@ -351,7 +351,7 @@ class _RideHeatmapState extends State { return LayoutBuilder( builder: (context, constraints) { // 计算每个砖块的大小 - final availableWidth = constraints.maxWidth - 88; + final availableWidth = constraints.maxWidth - 96; // 根据总天数动态计算每行显示的砖块数量 int blocksPerRow; diff --git a/lib/screens/visualization/visualization_page.dart b/lib/screens/visualization/visualization_page.dart index 8e41cf3..4e862a4 100644 --- a/lib/screens/visualization/visualization_page.dart +++ b/lib/screens/visualization/visualization_page.dart @@ -7,8 +7,7 @@ import 'departure_time_bar_chart.dart'; import 'check_in_time_histogram.dart'; import 'checked_in_reserved_pie_chart.dart'; import 'ride_heatmap.dart'; - -enum TimeRange { threeMonths, sixMonths, oneYear, all } +import '../../providers/visualization_settings_provider.dart'; class VisualizationSettingsPage extends StatefulWidget { @override @@ -18,7 +17,6 @@ class VisualizationSettingsPage extends StatefulWidget { class _VisualizationSettingsPageState extends State with SingleTickerProviderStateMixin { - TimeRange _selectedTimeRange = TimeRange.all; late PageController _pageController; int _currentPage = 0; @@ -67,12 +65,12 @@ class _VisualizationSettingsPageState extends State 'shortLabel': '3个月', }, TimeRange.sixMonths: { - 'icon': Icons.calendar_month, + 'icon': Icons.date_range_outlined, 'label': '过去半年', 'shortLabel': '半年', }, TimeRange.oneYear: { - 'icon': Icons.calendar_today_outlined, + 'icon': Icons.calendar_month, 'label': '过去一年', 'shortLabel': '一年', }, @@ -84,10 +82,12 @@ class _VisualizationSettingsPageState extends State }; List _filterRides(List rides) { + final selectedTimeRange = + Provider.of(context).selectedTimeRange; final now = DateTime.now(); late DateTime startDate; - switch (_selectedTimeRange) { + switch (selectedTimeRange) { case TimeRange.threeMonths: startDate = now.subtract(Duration(days: 90)); break; @@ -175,40 +175,45 @@ class _VisualizationSettingsPageState extends State centerTitle: true, surfaceTintColor: Colors.transparent, actions: [ - Padding( - padding: EdgeInsets.only(right: 8), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: _toggleFilter, - borderRadius: BorderRadius.circular(20), - child: Container( - height: 36, - padding: EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer.withOpacity(0.8), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - timeRangeInfo[_selectedTimeRange]!['shortLabel'], - style: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w500, + Center( + child: Padding( + padding: EdgeInsets.only(right: 8), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: _toggleFilter, + borderRadius: BorderRadius.circular(20), + child: Container( + height: 36, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: + theme.colorScheme.primaryContainer.withOpacity(0.8), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + timeRangeInfo[context + .watch() + .selectedTimeRange]!['shortLabel'], + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w500, + ), ), - ), - SizedBox(width: 4), - RotationTransition( - turns: _filterRotation, - child: Icon( - Icons.expand_more, - size: 20, - color: theme.colorScheme.onPrimaryContainer, + SizedBox(width: 4), + RotationTransition( + turns: _filterRotation, + child: Icon( + Icons.expand_more, + size: 20, + color: theme.colorScheme.onPrimaryContainer, + ), ), - ), - ], + ], + ), ), ), ), @@ -241,6 +246,11 @@ class _VisualizationSettingsPageState extends State title: '预约热力图', content: RideHeatmap(rides: rides), ), + _buildChartSection( + icon: Icons.pie_chart_outline, + title: '违约统计', + content: CheckedInReservedPieChart(rides: rides), + ), _buildChartSection( key: ValueKey('bar_${rides.length}'), icon: Icons.bar_chart_outlined, @@ -252,17 +262,22 @@ class _VisualizationSettingsPageState extends State title: '签到时间差(分钟)分布', content: CheckInTimeHistogram(rides: rides), ), - _buildChartSection( - icon: Icons.pie_chart_outline, - title: '违约统计', - content: CheckedInReservedPieChart(rides: rides), - ), ], ), ), _buildPageIndicator(), ], ), + if (_isFilterExpanded) + Positioned.fill( + child: GestureDetector( + onTap: _toggleFilter, + behavior: HitTestBehavior.opaque, + child: Container( + color: Colors.transparent, + ), + ), + ), if (_isFilterExpanded) Positioned( top: 0, @@ -287,33 +302,53 @@ class _VisualizationSettingsPageState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.filter_list, - size: 20, - color: theme.colorScheme.primary, - ), - SizedBox(width: 8), - Text( - '时间范围', - style: theme.textTheme.titleSmall?.copyWith( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, + child: Material( + color: Colors.transparent, + child: SizedBox( + width: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outlineVariant + .withOpacity(0.5), + width: 1, + ), ), ), - ], - ), + child: Row( + children: [ + Icon( + Icons.filter_list, + size: 20, + color: theme.colorScheme.primary, + ), + SizedBox(width: 8), + Text( + '时间范围', + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column( + children: TimeRange.values + .map((range) => _buildFilterOption(range)) + .toList(), + ), + ), + ], ), - ...TimeRange.values - .map((range) => _buildFilterOption(range)), - ], + ), ), ), ), @@ -326,43 +361,61 @@ class _VisualizationSettingsPageState extends State Widget _buildFilterOption(TimeRange range) { final theme = Theme.of(context); final info = timeRangeInfo[range]!; - final isSelected = _selectedTimeRange == range; + final visualizationSettings = + Provider.of(context); + final isSelected = visualizationSettings.selectedTimeRange == range; - return InkWell( - onTap: () { - setState(() { - _selectedTimeRange = range; - _toggleFilter(); - }); - }, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: isSelected ? theme.colorScheme.primaryContainer : null, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - info['icon'] as IconData, - size: 20, - color: isSelected - ? theme.colorScheme.primary - : theme.colorScheme.onSurfaceVariant, - ), - SizedBox(width: 12), - Text( - info['label'] as String, - style: theme.textTheme.bodyMedium?.copyWith( - color: isSelected - ? theme.colorScheme.primary - : theme.colorScheme.onSurface, - fontWeight: isSelected ? FontWeight.w500 : null, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + visualizationSettings.setTimeRange(range); + Future.delayed(Duration(milliseconds: 200), () { + _toggleFilter(); + }); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerHighest + .withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + info['icon'] as IconData, + size: 18, + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + ), ), - ), - SizedBox(width: 24), - ], + SizedBox(width: 12), + Expanded( + child: Text( + info['label'] as String, + style: theme.textTheme.bodyMedium?.copyWith( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.w500 : null, + ), + ), + ), + if (isSelected) + Icon( + Icons.check_rounded, + size: 20, + color: theme.colorScheme.primary, + ), + ], + ), ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 2eb3b91..af251e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: http: ^1.2.2 permission_handler: ^11.3.1 geolocator: ^13.0.1 - shared_preferences: ^2.0.6 + shared_preferences: ^2.2.0 cookie_jar: ^4.0.8 path_provider: ^2.1.2 intl: ^0.19.0 # 添加这一行