import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/theme/app_colors.dart'; import '../../shared/widgets/section_card.dart'; import '../../core/i18n/app_localizations.dart'; import '../shell/nav_bar_config.dart'; import 'home_controller.dart'; /// 工作台首页 /// /// 按角色展示不同区块: /// - 员工:轮播图 + 金刚区(发起/记录/报表)+ 个人快捷看板 /// - 经理:员工版 + 待审批红色角标卡片 + 部门快捷看板(3 卡片) /// - 财务:员工版 + 财务看盘(已支付/待付款/异常退回) /// - 管理员:员工版 + 金刚区"发布公告" + 财务看盘 class HomePage extends ConsumerWidget { const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final summaryAsync = ref.watch(homeSummaryProvider); final location = GoRouterState.of(context).uri.toString(); final l10n = AppLocalizations.of(context); if (location == '/') { ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('appName'), showBack: true, leadingIcon: Icons.close, ), ); } return summaryAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, _) => Center(child: Text(l10n.get('loadFailed'))), data: (summary) => SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB( AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.lg, ), child: Column( children: [ _buildBanner(ref, l10n), const SizedBox(height: AppSpacing.md), _buildInitiateGrid(context, summary, l10n), const SizedBox(height: AppSpacing.md), _buildRecordsGrid(context, l10n), const SizedBox(height: AppSpacing.md), _buildReportGrid(context, l10n), const SizedBox(height: AppSpacing.md), _buildDashboard(context, summary, l10n), ], ), ), ); } // =================================================================== // 轮播图 // =================================================================== Widget _buildBanner(WidgetRef ref, AppLocalizations l10n) { final banners = ref.watch(bannerProvider); return ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( height: 160, child: Swiper( autoplay: true, autoplayDelay: 3000, itemCount: banners.length, loop: true, pagination: const SwiperPagination( alignment: Alignment.bottomCenter, builder: TDSwiperPagination.dotsBar, ), onTap: (index) { final banner = banners[index]; if (banner.linkUrl != null) { // 有外链可跳转 } }, itemBuilder: (BuildContext context, int index) { return TDImage( assetUrl: banners[index].imageUrl, fit: BoxFit.cover, ); }, ), ), ); } // =================================================================== // 金刚区 — 发起 // =================================================================== Widget _buildInitiateGrid( BuildContext context, HomeSummary summary, AppLocalizations l10n, ) { final items = <_GridItem>[ _GridItem( icon: Icons.add_card_outlined, label: l10n.get('preApplication'), onTap: () => context.push('/expense-apply/apply'), ), _GridItem( icon: Icons.receipt_long_outlined, label: l10n.get('expenseReimbursement'), onTap: () => context.push('/expense/apply'), ), _GridItem( icon: Icons.directions_car_outlined, label: l10n.get('vehicleApplication'), onTap: () => context.push('/vehicle/apply'), ), _GridItem( icon: Icons.access_time_outlined, label: l10n.get('overtimeApplication'), onTap: () => context.push('/overtime/apply'), ), // 管理员额外显示"发布公告" if (summary.userRole == 'admin') _GridItem( icon: Icons.campaign_outlined, label: l10n.get('publishAnnouncement'), onTap: () => context.push('/announcement/create'), ), ]; return SectionCard( title: l10n.get('initiate'), actionText: l10n.get('more'), actionIcon: Icons.chevron_right, onActionTap: () => context.push('/expense-apply/list'), children: [_buildGrid(items)], ); } // =================================================================== // 金刚区 — 记录 // =================================================================== Widget _buildRecordsGrid(BuildContext context, AppLocalizations l10n) { final items = <_GridItem>[ _GridItem( icon: Icons.description_outlined, label: l10n.get('applicationRecords'), onTap: () => context.push('/expense-apply/list'), ), _GridItem( icon: Icons.receipt_outlined, label: l10n.get('expenseRecords'), onTap: () => context.push('/expense/list'), ), _GridItem( icon: Icons.access_time_outlined, label: l10n.get('overtimeRecords'), onTap: () => context.push('/overtime/list'), ), _GridItem( icon: Icons.directions_car_outlined, label: l10n.get('vehicleRecords'), onTap: () => context.push('/vehicle/list'), ), _GridItem( icon: Icons.edit_note_outlined, label: l10n.get('outingLogs'), onTap: () => context.push('/outing-log/list'), ), _GridItem( icon: Icons.campaign_outlined, label: l10n.get('companyAnnouncements'), onTap: () => context.push('/announcement/list'), ), ]; return SectionCard( title: l10n.get('records'), actionText: l10n.get('more'), actionIcon: Icons.chevron_right, onActionTap: () => context.push('/expense-apply/list'), children: [_buildGrid(items)], ); } // =================================================================== // 金刚区 — 报表 // =================================================================== Widget _buildReportGrid(BuildContext context, AppLocalizations l10n) { final items = <_GridItem>[ _GridItem( icon: Icons.bar_chart_outlined, label: l10n.get('reportExpenseApply'), onTap: () => context.push('/report/expense-apply-detail'), ), _GridItem( icon: Icons.pie_chart_outline, label: l10n.get('reportExpense'), onTap: () => context.push('/report/expense-detail'), ), _GridItem( icon: Icons.query_stats_outlined, label: l10n.get('reportOvertime'), onTap: () => context.push('/report/overtime-detail'), ), _GridItem( icon: Icons.map_outlined, label: l10n.get('reportVehicle'), onTap: () => context.push('/report/vehicle-detail'), ), _GridItem( icon: Icons.explore_outlined, label: l10n.get('reportOutingLog'), onTap: () => context.push('/report/outing-log-detail'), ), ]; return SectionCard( title: l10n.get('reports'), showAction: false, children: [_buildGrid(items)], ); } // =================================================================== // 宫格构建器(4 列,自动换行) // =================================================================== Widget _buildGrid(List<_GridItem> items) { return LayoutBuilder( builder: (context, constraints) { final itemWidth = constraints.maxWidth / 4; final runSpacing = AppSpacing.md; final spacing = 0.0; return Wrap( runSpacing: runSpacing, spacing: spacing, children: items.map((item) { return SizedBox( width: itemWidth, child: _buildGridItem(item), ); }).toList(), ); }, ); } Widget _buildGridItem(_GridItem item) { return GestureDetector( onTap: item.onTap, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(10), ), child: Icon(item.icon, size: 22, color: AppColors.primary), ), const SizedBox(height: AppSpacing.xs), Text( item.label, style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textSecondary, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } // =================================================================== // 角色判断 & 看板分发 // =================================================================== Widget _buildDashboard( BuildContext context, HomeSummary summary, AppLocalizations l10n, ) { switch (summary.userRole) { case 'admin': return _buildFinanceDashboard(context, summary, l10n); case 'finance': return _buildFinanceDashboard(context, summary, l10n); case 'manager': return _buildManagerDashboard(context, summary, l10n); default: return _buildEmployeeDashboard(context, summary, l10n); } } // =================================================================== // 员工版:个人快捷看板(2 卡片) // =================================================================== Widget _buildEmployeeDashboard( BuildContext context, HomeSummary summary, AppLocalizations l10n, ) { return SectionCard( title: l10n.get('myDashboard'), showAction: false, children: [ Row( children: [ Expanded( child: _buildStatCard( title: l10n.get('monthlyTotalExpense'), value: '¥${_formatAmount(summary.monthlyReimbursement)}', valueColor: AppColors.amountPrimary, onTap: () => context.push('/expense/list'), ), ), const SizedBox(width: AppSpacing.md), Expanded( child: _buildStatCard( title: l10n.get('monthlySubmitted'), value: '${summary.monthlySubmittedCount} 笔', valueColor: AppColors.textPrimary, onTap: () => context.push('/expense-apply/list'), ), ), ], ), ], ); } // =================================================================== // 经理版:待审批角标 + 部门快捷看板(3 卡片) // =================================================================== Widget _buildManagerDashboard( BuildContext context, HomeSummary summary, AppLocalizations l10n, ) { return Column( children: [ // 待审批卡片(红色角标) GestureDetector( onTap: () => context.push('/messages'), child: Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.dangerBg, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.task_alt, color: AppColors.danger, size: 24, ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( l10n.get('pendingApproval'), style: const TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ), if (summary.pendingApprovalCount > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppColors.danger, borderRadius: BorderRadius.circular(10), ), child: Text( '${summary.pendingApprovalCount}', style: const TextStyle( fontSize: AppFontSizes.caption, color: Colors.white, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: AppSpacing.xs), const Icon( Icons.chevron_right, color: AppColors.textPlaceholder, size: 20, ), ], ), ), ), const SizedBox(height: AppSpacing.md), // 部门快捷看板(3 卡片) SectionCard( title: l10n.get('deptDashboard'), showAction: false, children: [ Row( children: [ Expanded( child: _buildStatCard( title: l10n.get('deptMonthlyReimbursement'), value: '¥${_formatAmount(summary.deptMonthlyReimbursement)}', valueColor: AppColors.amountPrimary, onTap: () => context.push('/expense/list'), ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: _buildStatCard( title: l10n.get('deptMonthlySubmitted'), value: '${summary.deptMonthlySubmittedCount} 笔', valueColor: AppColors.textPrimary, onTap: () => context.push('/expense-apply/list'), ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: _buildStatCard( title: l10n.get('deptPendingDocuments'), value: '${summary.deptPendingDocuments} 笔', valueColor: AppColors.warning, onTap: () => context.push('/expense-apply/list'), ), ), ], ), ], ), ], ); } // =================================================================== // 财务/管理员版:财务看盘(3 大数字卡片) // =================================================================== Widget _buildFinanceDashboard( BuildContext context, HomeSummary summary, AppLocalizations l10n, ) { return SectionCard( title: l10n.get('financeDashboard'), showAction: false, children: [ Row( children: [ Expanded( child: _buildStatCard( title: l10n.get('paidTotal'), value: '¥${_formatAmount(summary.paidTotal)}', valueColor: AppColors.success, valueFontSize: 18, onTap: () => context.push('/expense/list'), ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: _buildStatCard( title: l10n.get('pendingPaymentTotal'), value: '¥${_formatAmount(summary.pendingPaymentTotal)}', valueColor: AppColors.warning, valueFontSize: 18, onTap: () => context.push('/expense/list'), ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: _buildStatCard( title: l10n.get('abnormalReturns'), value: '¥${_formatAmount(summary.abnormalReturns)}', valueColor: AppColors.danger, valueFontSize: 18, onTap: () => context.push('/expense/list'), ), ), ], ), ], ); } // =================================================================== // 统计数值卡片 // =================================================================== Widget _buildStatCard({ required String title, required String value, required Color valueColor, double valueFontSize = 22, VoidCallback? onTap, }) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric( vertical: AppSpacing.sm, horizontal: AppSpacing.sm, ), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(8), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( value, style: TextStyle( fontSize: valueFontSize, fontWeight: FontWeight.w700, color: valueColor, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: AppSpacing.xs), Text( title, style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textSecondary, ), textAlign: TextAlign.center, ), ], ), ), ); } /// 格式化金额:保留两位小数,去除末尾多余的 0 String _formatAmount(double amount) { final str = amount.toStringAsFixed(2); // 保留两位小数的标准格式 return str; } } /// 宫格项数据类 class _GridItem { final IconData icon; final String label; final VoidCallback onTap; const _GridItem({ required this.icon, required this.label, required this.onTap, }); }