Spaces:
Sleeping
Sleeping
| import 'package:flutter/material.dart'; | |
| import 'package:provider/provider.dart'; | |
| import 'package:intl/intl.dart'; | |
| import 'models.dart'; | |
| import 'logic.dart'; | |
| void main() { | |
| runApp( | |
| ChangeNotifierProvider( | |
| create: (context) => TimeManager(), | |
| child: const TimeVoyagerApp(), | |
| ), | |
| ); | |
| } | |
| class TimeVoyagerApp extends StatelessWidget { | |
| const TimeVoyagerApp({super.key}); | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'TimeVoyager', | |
| theme: ThemeData( | |
| colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), | |
| useMaterial3: true, | |
| ), | |
| home: const ProjectListPage(), | |
| ); | |
| } | |
| } | |
| class ProjectListPage extends StatelessWidget { | |
| const ProjectListPage({super.key}); | |
| Widget build(BuildContext context) { | |
| final manager = context.watch<TimeManager>(); | |
| return Scaffold( | |
| appBar: AppBar( | |
| title: const Text('TimeVoyager'), | |
| backgroundColor: Theme.of(context).colorScheme.inversePrimary, | |
| ), | |
| body: ListView.builder( | |
| itemCount: manager.projects.length, | |
| itemBuilder: (context, index) { | |
| final project = manager.projects[index]; | |
| final isActive = manager.activeEntry?.projectId == project.id; | |
| return Card( | |
| margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | |
| child: ListTile( | |
| leading: CircleAvatar( | |
| backgroundColor: isActive ? Colors.green : Colors.blueGrey, | |
| child: Icon(isActive ? Icons.play_arrow : Icons.folder), | |
| ), | |
| title: Text(project.name), | |
| subtitle: Text('${project.hourlyRate} ${project.currency} / 小时'), | |
| trailing: isActive | |
| ? const Text('正在计时...', style: TextStyle(color: Colors.green)) | |
| : const Icon(Icons.chevron_right), | |
| onTap: () { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute( | |
| builder: (context) => ProjectDetailPage(projectId: project.id), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ); | |
| }, | |
| ), | |
| floatingActionButton: FloatingActionButton( | |
| onPressed: () { | |
| // TODO: Implement Add Project Dialog | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('演示模式:默认项目已预置')), | |
| ); | |
| }, | |
| child: const Icon(Icons.add), | |
| ), | |
| ); | |
| } | |
| } | |
| class ProjectDetailPage extends StatelessWidget { | |
| final String projectId; | |
| const ProjectDetailPage({super.key, required this.projectId}); | |
| Widget build(BuildContext context) { | |
| final manager = context.watch<TimeManager>(); | |
| final project = manager.getProjectById(projectId); | |
| if (project == null) return const Scaffold(body: Center(child: Text('项目不存在'))); | |
| final isActive = manager.activeEntry?.projectId == project.id; | |
| return Scaffold( | |
| appBar: AppBar(title: Text(project.name)), | |
| body: Padding( | |
| padding: const EdgeInsets.all(24.0), | |
| child: Column( | |
| children: [ | |
| _buildStatusCard(context, project, manager, isActive), | |
| const SizedBox(height: 40), | |
| _buildControlButtons(context, project, manager, isActive), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildStatusCard(BuildContext context, Project project, TimeManager manager, bool isActive) { | |
| final duration = isActive ? manager.currentDuration : Duration.zero; | |
| final earnings = isActive ? manager.currentEarnings : 0.0; | |
| final formattedTime = _formatDuration(duration); | |
| final formattedEarnings = NumberFormat.currency(symbol: project.currency == 'CNY' ? '¥' : '\$') | |
| .format(earnings); | |
| return Container( | |
| width: double.infinity, | |
| padding: const EdgeInsets.all(32), | |
| decoration: BoxDecoration( | |
| color: isActive ? Colors.blue.shade50 : Colors.grey.shade100, | |
| borderRadius: BorderRadius.circular(24), | |
| border: Border.all( | |
| color: isActive ? Colors.blue.shade200 : Colors.transparent, | |
| width: 2, | |
| ), | |
| ), | |
| child: Column( | |
| children: [ | |
| Text( | |
| isActive ? '工作中' : '就绪', | |
| style: TextStyle( | |
| color: isActive ? Colors.blue : Colors.grey, | |
| fontWeight: FontWeight.bold, | |
| letterSpacing: 1.2, | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| Text( | |
| formattedTime, | |
| style: const TextStyle( | |
| fontSize: 64, | |
| fontFamily: 'Monospace', | |
| fontWeight: FontWeight.w300, | |
| ), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| formattedEarnings, | |
| style: TextStyle( | |
| fontSize: 32, | |
| fontWeight: FontWeight.bold, | |
| color: isActive ? Colors.green : Colors.grey, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildControlButtons(BuildContext context, Project project, TimeManager manager, bool isActive) { | |
| if (isActive) { | |
| return SizedBox( | |
| width: double.infinity, | |
| height: 60, | |
| child: ElevatedButton.icon( | |
| onPressed: () => manager.stopTracking(), | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: Colors.red.shade100, | |
| foregroundColor: Colors.red, | |
| ), | |
| icon: const Icon(Icons.stop_circle), | |
| label: const Text('停止计时', style: TextStyle(fontSize: 20)), | |
| ), | |
| ); | |
| } else { | |
| // Check if another project is running | |
| final isOtherRunning = manager.isTracking && !isActive; | |
| return SizedBox( | |
| width: double.infinity, | |
| height: 60, | |
| child: ElevatedButton.icon( | |
| onPressed: isOtherRunning | |
| ? () { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('请先停止当前正在进行的项目')), | |
| ); | |
| } | |
| : () => manager.startTracking(project), | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: isOtherRunning ? Colors.grey : Colors.green.shade100, | |
| foregroundColor: isOtherRunning ? Colors.white : Colors.green, | |
| ), | |
| icon: const Icon(Icons.play_circle), | |
| label: Text( | |
| isOtherRunning ? '其他项目进行中' : '开始工作', | |
| style: const TextStyle(fontSize: 20) | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| String _formatDuration(Duration d) { | |
| String twoDigits(int n) => n.toString().padLeft(2, "0"); | |
| String twoDigitMinutes = twoDigits(d.inMinutes.remainder(60)); | |
| String twoDigitSeconds = twoDigits(d.inSeconds.remainder(60)); | |
| return "${twoDigits(d.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; | |
| } | |
| } | |