| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- 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';
- 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(16, 16, 16, 24),
- child: Column(
- children: [
- // Banner 区域
- _buildBanner(),
- const SizedBox(height: 16),
- // 发起
- SectionCard(
- title: l10n.get('initiate'),
- actionText: l10n.get('more'),
- actionIcon: Icons.chevron_right,
- onActionTap: () => context.push('/expense-apply/list'),
- children: [
- _buildGrid([
- _GridItem(
- icon: Icons.add_card,
- label: l10n.get('preApplication'),
- onTap: () => context.push('/expense-apply/apply'),
- ),
- _GridItem(
- icon: Icons.receipt_long,
- label: l10n.get('expenseReimbursement'),
- onTap: () => context.push('/expense/apply'),
- ),
- _GridItem(
- icon: Icons.directions_car,
- label: l10n.get('vehicleApplication'),
- onTap: () => context.push('/vehicle/apply'),
- ),
- _GridItem(
- icon: Icons.access_time,
- label: l10n.get('overtimeApplication'),
- onTap: () => context.push('/overtime/apply'),
- ),
- ]),
- ],
- ),
- const SizedBox(height: 16),
- // 记录
- SectionCard(
- title: l10n.get('records'),
- actionText: l10n.get('more'),
- actionIcon: Icons.chevron_right,
- onActionTap: () => context.push('/expense-apply/list'),
- children: [
- _buildGrid([
- _GridItem(
- icon: Icons.folder_copy,
- label: l10n.get('applicationRecords'),
- onTap: () => context.push('/expense-apply/list'),
- ),
- _GridItem(
- icon: Icons.article_outlined,
- label: l10n.get('expenseRecords'),
- onTap: () => context.push('/expense/list'),
- ),
- _GridItem(
- icon: Icons.push_pin_outlined,
- label: l10n.get('outingLogs'),
- onTap: () => context.push('/outing-log/list'),
- ),
- _GridItem(
- icon: Icons.campaign,
- label: l10n.get('companyAnnouncements'),
- onTap: () => context.push('/announcement/list'),
- ),
- ]),
- ],
- ),
- const SizedBox(height: 16),
- // 我的快捷看板
- SectionCard(
- title: l10n.get('myDashboard'),
- showAction: false,
- children: [_buildStatsRow(summary, l10n)],
- ),
- ],
- ),
- ),
- );
- }
- static const _banners = [
- 'assets/img/banner_1.png',
- 'assets/img/banner_2.png',
- 'assets/img/banner_3.png',
- ];
- Widget _buildBanner() {
- return ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: SizedBox(
- height: 160,
- child: Swiper(
- autoplay: true,
- itemCount: _banners.length,
- loop: true,
- pagination: const SwiperPagination(
- alignment: Alignment.bottomCenter,
- builder: TDSwiperPagination.dotsBar,
- ),
- itemBuilder: (BuildContext context, int index) {
- return TDImage(assetUrl: _banners[index], fit: BoxFit.cover);
- },
- ),
- ),
- );
- }
- Widget _buildGrid(List<_GridItem> items) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: items.map((item) => _buildGridItem(item)).toList(),
- );
- }
- Widget _buildGridItem(_GridItem item) {
- return GestureDetector(
- onTap: item.onTap,
- child: SizedBox(
- width: 64,
- 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: 6),
- Text(
- item.label,
- style: const TextStyle(
- fontSize: AppFontSizes.caption,
- color: AppColors.textSecondary,
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildStatsRow(HomeSummary summary, AppLocalizations l10n) {
- final pendingTotal =
- summary.expensePending +
- summary.overtimePending +
- summary.vehiclePending;
- final submittedTotal =
- summary.expensePending +
- summary.overtimePending +
- summary.vehiclePending +
- summary.expenseApplyPending;
- return Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _buildStat(
- '¥${summary.totalCount * 100}',
- l10n.get('monthlyTotalExpense'),
- AppColors.amountPrimary,
- ),
- _buildStat(
- '$submittedTotal',
- l10n.get('monthlySubmitted'),
- AppColors.textPrimary,
- ),
- _buildStat(
- '$pendingTotal',
- l10n.get('pendingDocuments'),
- AppColors.warning,
- ),
- ],
- );
- }
- Widget _buildStat(String value, String label, Color valueColor) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- value,
- style: TextStyle(
- fontSize: 22,
- fontWeight: FontWeight.w700,
- color: valueColor,
- ),
- ),
- const SizedBox(height: AppSpacing.xs),
- Text(
- label,
- style: const TextStyle(
- fontSize: AppFontSizes.caption,
- color: AppColors.textSecondary,
- ),
- ),
- ],
- );
- }
- }
- class _GridItem {
- final IconData icon;
- final String label;
- final VoidCallback onTap;
- const _GridItem({
- required this.icon,
- required this.label,
- required this.onTap,
- });
- }
|