import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/page_header.dart' as widgets; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../calendar/data/calendar_api.dart'; import '../../data/todo_api.dart'; class TodoDetailScreen extends StatefulWidget { final String todoId; const TodoDetailScreen({super.key, required this.todoId}); @override State createState() => _TodoDetailScreenState(); } class _TodoDetailScreenState extends State { final TodoApi _todoApi = sl(); TodoResponse? _todo; bool _isLoading = true; String? _error; @override void initState() { super.initState(); _loadTodo(); } Future _loadTodo() async { setState(() { _isLoading = true; _error = null; }); try { final todo = await _todoApi.getTodo(widget.todoId); setState(() { _todo = todo; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } String _getPriorityLabel(int priority) { switch (priority) { case 1: return '重要紧急'; case 2: return '重要不紧急'; case 3: return '紧急不重要'; case 4: return '不紧急不重要'; default: return '未知'; } } Color _getPriorityColor(int priority) { switch (priority) { case 1: return AppColors.g1Text; case 2: return AppColors.g3Text; case 3: return AppColors.g2Text; default: return AppColors.slate500; } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, body: SafeArea( child: Column( children: [ _buildHeader(context), Expanded(child: _buildContent()), ], ), ), ); } Widget _buildHeader(BuildContext context) { return SizedBox( height: 64, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 8), child: Row( children: [ widgets.BackButton(onPressed: () => Navigator.of(context).pop()), const Spacer(), if (_todo != null) ...[ IconButton( onPressed: _editTodo, icon: const Icon( LucideIcons.pencil, size: 20, color: AppColors.slate600, ), ), IconButton( onPressed: _deleteTodo, icon: const Icon( LucideIcons.trash2, size: 20, color: Colors.red, ), ), ], ], ), ), ); } Widget _buildContent() { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('加载失败: $_error', style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), AppButton(text: '重试', onPressed: _loadTodo), ], ), ); } if (_todo == null) { return const Center(child: Text('待办不存在')); } return Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 20), child: ListView( children: [ _buildMainCard(), const SizedBox(height: 12), if (_todo!.scheduleItems.isNotEmpty) ...[ Text( '日历事件卡片', style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.slate500, ), ), const SizedBox(height: 10), ..._todo!.scheduleItems.map( (item) => _buildEventCard( id: item.id, title: item.title, time: _formatEventTime(item.startAt, item.endAt), borderColor: AppColors.todoEventBorder1, onTap: () => context.push('/calendar/events/${item.id}'), ), ), ], ], ), ); } String _formatEventTime(DateTime start, DateTime? end) { final startStr = '${start.year}年${start.month}月${start.day}日 ${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}'; if (end != null) { final endStr = '${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}'; return '$startStr - $endStr'; } return startStr; } Widget _buildMainCard() { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.todoCardBg, borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.todoDetailCardBorder, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _todo!.title, style: const TextStyle( fontFamily: 'Inter', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.slate900, ), ), const SizedBox(height: 4), Text( _buildSubtitle(), style: const TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.slate500, ), ), if (_todo!.description != null && _todo!.description!.isNotEmpty) ...[ const SizedBox(height: 8), Text( _todo!.description!, style: const TextStyle( fontFamily: 'Inter', fontSize: 13, color: AppColors.slate600, ), ), ], const SizedBox(height: 8), Container(height: 1, color: AppColors.border), const SizedBox(height: 8), _buildInfoRow( label: '所属象限', value: _getPriorityLabel(_todo!.priority), valueColor: _getPriorityColor(_todo!.priority), ), const SizedBox(height: 8), _buildInfoRow( label: '关联日历事件', value: '${_todo!.scheduleItems.length}个', valueColor: AppColors.g3Text, ), const SizedBox(height: 8), _buildInfoRow( label: '状态', value: _todo!.status == 'done' ? '已完成' : '进行中', valueColor: _todo!.status == 'done' ? AppColors.success : AppColors.blue600, ), ], ), ); } String _buildSubtitle() { final parts = []; if (_todo!.dueAt != null) { final due = _todo!.dueAt!; parts.add( '截止 ${due.month}月${due.day}日 ${due.hour.toString().padLeft(2, '0')}:${due.minute.toString().padLeft(2, '0')}', ); } if (_todo!.scheduleItems.isNotEmpty) { parts.add('已拆分为${_todo!.scheduleItems.length}个日历事件'); } else { parts.add('未关联日历事件'); } return parts.join(' · '); } Widget _buildInfoRow({ required String label, required String value, required Color valueColor, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.slate400, ), ), Text( value, style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w700, color: valueColor, ), ), ], ); } Widget _buildEventCard({ required String id, required String title, required String time, required Color borderColor, VoidCallback? onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: double.infinity, padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: AppColors.todoCardBg, borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: 'Inter', fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), Text( time, style: const TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.slate500, ), ), ], ), ), ); } void _editTodo() async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _EditTodoSheet(todo: _todo!), ); if (result != null) { try { await _todoApi.updateTodo( _todo!.id, title: result['title'] as String, description: result['description'] as String?, priority: result['priority'] as int, scheduleItemIds: result['schedule_item_ids'] as List?, ); await _loadTodo(); } catch (e) { if (mounted) { Toast.show(context, '更新失败: $e', type: ToastType.error); } } } } void _deleteTodo() async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: const Text('确定要删除这个待办吗?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirm == true) { try { await _todoApi.deleteTodo(_todo!.id); if (mounted) { context.pop(); } } catch (e) { if (mounted) { Toast.show(context, '删除失败: $e', type: ToastType.error); } } } } } class _EditTodoSheet extends StatefulWidget { final TodoResponse todo; const _EditTodoSheet({required this.todo}); @override State<_EditTodoSheet> createState() => _EditTodoSheetState(); } class _EditTodoSheetState extends State<_EditTodoSheet> { late TextEditingController _titleController; late TextEditingController _descriptionController; late int _priority; late Set _selectedScheduleItems; @override void initState() { super.initState(); _titleController = TextEditingController(text: widget.todo.title); _descriptionController = TextEditingController( text: widget.todo.description ?? '', ); _priority = widget.todo.priority; _selectedScheduleItems = widget.todo.scheduleItems.map((e) => e.id).toSet(); } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( height: MediaQuery.of(context).size.height * 0.85, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '编辑待办', style: TextStyle( fontFamily: 'Inter', fontSize: 20, fontWeight: FontWeight.w700, ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(LucideIcons.x), ), ], ), const SizedBox(height: 20), TextField( controller: _titleController, decoration: const InputDecoration( labelText: '标题', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _descriptionController, decoration: const InputDecoration( labelText: '描述(可选)', border: OutlineInputBorder(), ), maxLines: 2, ), const SizedBox(height: 16), const Text( '优先级', style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Row( children: [ _PriorityChip( label: '重要紧急', selected: _priority == 1, color: AppColors.g1Border, onTap: () => setState(() => _priority = 1), ), const SizedBox(width: 8), _PriorityChip( label: '紧急不重要', selected: _priority == 3, color: AppColors.g2Border, onTap: () => setState(() => _priority = 3), ), const SizedBox(width: 8), _PriorityChip( label: '重要不紧急', selected: _priority == 2, color: AppColors.g3Border, onTap: () => setState(() => _priority = 2), ), ], ), ], ), ), Expanded( child: FutureBuilder( future: _loadScheduleItems(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('加载失败: ${snapshot.error}')); } final items = snapshot.data ?? []; if (items.isEmpty) { return const Center(child: Text('暂无日历事件')); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; final isSelected = _selectedScheduleItems.contains(item.id); return CheckboxListTile( title: Text(item.title), subtitle: Text(_formatDate(item.startAt)), value: isSelected, onChanged: (value) { setState(() { if (value == true) { _selectedScheduleItems.add(item.id); } else { _selectedScheduleItems.remove(item.id); } }); }, ); }, ); }, ), ), Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: AppButton( text: '保存', onPressed: () { if (_titleController.text.trim().isEmpty) { Toast.show(context, '请输入标题', type: ToastType.warning); return; } Navigator.of(context).pop({ 'title': _titleController.text.trim(), 'description': _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(), 'priority': _priority, 'schedule_item_ids': _selectedScheduleItems.toList(), }); }, ), ), ), ], ), ); } Future> _loadScheduleItems() async { final calendarApi = sl(); final now = DateTime.now(); final start = now.subtract(const Duration(days: 30)); final end = now.add(const Duration(days: 90)); final items = await calendarApi.listByRange(startAt: start, endAt: end); return items .map( (e) => _ScheduleItemSimple(id: e.id, title: e.title, startAt: e.startAt), ) .toList(); } String _formatDate(DateTime dt) { return '${dt.year}年${dt.month}月${dt.day}日 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } } class _ScheduleItemSimple { final String id; final String title; final DateTime startAt; _ScheduleItemSimple({ required this.id, required this.title, required this.startAt, }); } class _PriorityChip extends StatelessWidget { final String label; final bool selected; final Color color; final VoidCallback onTap; const _PriorityChip({ required this.label, required this.selected, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: selected ? color.withValues(alpha: 0.2) : Colors.transparent, border: Border.all( color: selected ? color : AppColors.slate300, width: selected ? 2 : 1, ), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, color: selected ? color : AppColors.slate600, ), ), ), ); } }