| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- 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,
- });
- }
|