import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/status_banner.dart'; import '../../shared/widgets/approval_timeline.dart'; import 'overtime_model.dart'; import '../../core/i18n/app_localizations.dart'; import 'overtime_list_controller.dart'; class OvertimeDetailPage extends ConsumerWidget { final String id; const OvertimeDetailPage({super.key, required this.id}); @override Widget build(BuildContext context, WidgetRef ref) { final overtime = mockOvertimes.firstWhere( (e) => e.id == id, orElse: () => mockOvertimes.first, ); final (icon, color, statusText) = _statusProps(overtime.status); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('overtimeDetail'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusBanner( icon: icon, statusText: statusText, subText: _statusSubText(overtime), color: color, ), const SizedBox(height: 8), Text( '提交时间:${du.DateUtils.formatDateTime(overtime.createTime)}', style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textPlaceholder, ), ), const SizedBox(height: 16), _buildInfoSection(overtime), const SizedBox(height: 16), _buildReasonSection(overtime), const SizedBox(height: 16), if (overtime.approvalRecords.isNotEmpty || overtime.approvalChain.isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: ApprovalTimeline( records: overtime.approvalRecords, chain: overtime.approvalChain, currentApproverId: overtime.currentApproverId, ), ), ], ), ), ), _buildActionBar(context, overtime), ], ); } Widget _buildInfoSection(OvertimeModel overtime) { return Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ _infoRow('申请人', overtime.applicantName), _infoRow('所属部门', overtime.deptName), _infoRow('加班类型', overtime.otType), _infoRow('补偿方式', overtime.compensationType), _infoRow('净加班时长', '${overtime.otHours.toStringAsFixed(1)}小时'), _infoRow('开始时间', du.DateUtils.formatDateTime(overtime.startTime)), _infoRow('结束时间', du.DateUtils.formatDateTime(overtime.endTime)), ], ), ); } Widget _infoRow(String label, String value) { return Container( height: 44, padding: const EdgeInsets.symmetric(vertical: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), Text( value, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), ), ], ), ); } Widget _buildReasonSection(OvertimeModel overtime) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '加班事由', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), Text( overtime.reason.isEmpty ? '无' : overtime.reason, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), ), ], ), ); } Widget _buildActionBar(BuildContext context, OvertimeModel overtime) { final showWithdraw = overtime.status == 'pending' || overtime.status == 'draft'; if (!showWithdraw) return const SizedBox.shrink(); return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: const BoxDecoration(color: AppColors.bgCard), child: Row( children: [ const Spacer(), SizedBox( height: 40, child: Material( color: AppColors.primary, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: () { // 撤回逻辑,保留接口 }, borderRadius: BorderRadius.circular(22), child: const Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 32), child: Text( '撤回申请', style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ), ), const Spacer(), ], ), ); } (IconData, Color, String) _statusProps(String status) { switch (status) { case 'approved': return (Icons.check_circle, AppColors.success, '已通过'); case 'rejected': return (Icons.cancel, AppColors.danger, '已拒绝'); case 'draft': return (Icons.edit_note, AppColors.statusGray, '草稿'); case 'revoked': return (Icons.cancel_outlined, AppColors.revokedText, '已撤回'); default: return (Icons.access_time, AppColors.warning, '审批中'); } } String _statusSubText(OvertimeModel overtime) { switch (overtime.status) { case 'pending': return overtime.currentApproverId.isNotEmpty ? '当前审批人:${overtime.currentApproverId}' : '等待审批'; case 'approved': final last = overtime.approvalRecords.isNotEmpty ? overtime.approvalRecords.last.approverName : ''; return last.isNotEmpty ? '审批人:$last' : '已通过'; case 'rejected': final last = overtime.approvalRecords.isNotEmpty ? overtime.approvalRecords.last.approverName : ''; return last.isNotEmpty ? '拒绝人:$last' : '已拒绝'; default: return ''; } } }