gcx/frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart

478 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/coupon_card.dart';
import '../../../ai_agent/presentation/widgets/ai_fab.dart';
import '../widgets/receive_coupon_sheet.dart';
import '../../../../app/i18n/app_localizations.dart';
import '../../../../core/services/coupon_service.dart';
import '../../data/models/coupon_model.dart';
import '../../data/models/holdings_summary_model.dart';
/// 首页 - 轻量持仓卡 + 分类网格 + AI推荐 + 精选券
///
/// Tab导航首页/交易/消息/我的
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final CouponApiService _couponService = CouponApiService();
List<CouponModel> _featuredCoupons = [];
HoldingsSummaryModel? _holdingsSummary;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final results = await Future.wait([
_couponService.getFeaturedCoupons(limit: 10),
_couponService.getHoldingsSummary(),
]);
if (mounted) {
setState(() {
_featuredCoupons = results[0] as List<CouponModel>;
_holdingsSummary = results[1] as HoldingsSummaryModel;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
RefreshIndicator(
onRefresh: _loadData,
child: CustomScrollView(
slivers: [
// Floating App Bar
SliverAppBar(
floating: true,
pinned: false,
automaticallyImplyLeading: false,
backgroundColor: AppColors.background,
elevation: 0,
toolbarHeight: 60,
title: _buildSearchBar(context),
actions: [
IconButton(
icon: const Icon(Icons.qr_code_scanner_rounded, size: 24),
onPressed: () {},
color: AppColors.textPrimary,
),
],
),
// Lightweight Position Card (持仓)
SliverToBoxAdapter(child: _buildWalletCard(context)),
// Category Grid (8 items, 4x2)
SliverToBoxAdapter(child: _buildCategoryGrid(context)),
// AI Smart Suggestions
SliverToBoxAdapter(child: _buildAiSuggestions(context)),
// Section: Featured Coupons
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(context.t('home.featuredCoupons'), style: AppTypography.h2),
GestureDetector(
onTap: () {},
child: Text(context.t('home.viewAllCoupons'), style: AppTypography.labelSmall.copyWith(
color: AppColors.primary,
)),
),
],
),
),
),
// Coupon List
if (_isLoading)
SliverPadding(
padding: AppSpacing.pagePadding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildSkeletonCard(),
),
childCount: 3,
),
),
)
else if (_error != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Icon(Icons.cloud_off_rounded, size: 48, color: AppColors.textTertiary),
const SizedBox(height: 12),
Text(
context.t('home.loadFailed'),
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary),
),
const SizedBox(height: 12),
TextButton(
onPressed: _loadData,
child: Text(context.t('home.retry')),
),
],
),
),
)
else if (_featuredCoupons.isEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(40),
child: Center(
child: Text(
context.t('home.noCoupons'),
style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
),
),
),
)
else
SliverPadding(
padding: AppSpacing.pagePadding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final coupon = _featuredCoupons[index];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: CouponCard(
brandName: coupon.brandName ?? '',
couponName: coupon.name,
faceValue: coupon.faceValue,
currentPrice: coupon.currentPrice,
creditRating: coupon.creditRating ?? '',
expiryDate: coupon.expiryDate,
onTap: () {
Navigator.pushNamed(context, '/coupon/detail', arguments: coupon.id);
},
),
);
},
childCount: _featuredCoupons.length,
),
),
),
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
],
),
),
// AI FAB
Positioned(
right: 20,
bottom: 100,
child: AiFab(
unreadCount: 3,
onTap: () {
Navigator.pushNamed(context, '/ai-chat');
},
),
),
],
),
);
}
Widget _buildSkeletonCard() {
return Container(
height: 100,
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusMd,
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
Widget _buildSearchBar(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/search');
},
child: Container(
height: 40,
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusFull,
border: Border.all(color: AppColors.borderLight),
),
child: Row(
children: [
const SizedBox(width: 14),
const Icon(Icons.search_rounded, size: 20, color: AppColors.textTertiary),
const SizedBox(width: 8),
Expanded(
child: Text(
context.t('home.searchHint'),
style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
// ============================================================
// Lightweight Position Card (持仓卡片)
// 点击非快捷入口区域打开完整持仓页面
// ============================================================
Widget _buildWalletCard(BuildContext context) {
final count = _holdingsSummary?.count ?? 0;
final totalValue = _holdingsSummary?.totalFaceValue ?? 0;
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/wallet/coupons'),
child: Container(
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppColors.cardGradient,
borderRadius: AppSpacing.borderRadiusLg,
boxShadow: AppSpacing.shadowPrimary,
),
child: Column(
children: [
// Top row: wallet info + receive button
Row(
children: [
const Icon(Icons.inventory_2_rounded,
size: 20, color: Colors.white),
const SizedBox(width: 8),
Text(context.t('home.position'),
style: AppTypography.labelMedium.copyWith(color: Colors.white)),
const Spacer(),
// Summary
Text('${context.t('home.hold')} ',
style: AppTypography.bodySmall.copyWith(
color: Colors.white.withValues(alpha: 0.7),
)),
Text('$count',
style: AppTypography.h3.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
)),
Text(' ${context.t('home.couponUnit')} ${context.t('home.totalValue')} ',
style: AppTypography.bodySmall.copyWith(
color: Colors.white.withValues(alpha: 0.7),
)),
Text('\$${totalValue.toStringAsFixed(0)}',
style: AppTypography.h3.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
)),
const SizedBox(width: 4),
Icon(Icons.chevron_right_rounded,
size: 18, color: Colors.white.withValues(alpha: 0.7)),
],
),
const SizedBox(height: 14),
// Quick action entries
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildQuickAction(context, Icons.qr_code_rounded, context.t('home.receive'), () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const ReceiveCouponSheet(),
);
}),
_buildQuickAction(context, Icons.card_giftcard_rounded, context.t('home.transfer'), () {
Navigator.pushNamed(context, '/transfer');
}),
_buildQuickAction(context, Icons.sell_rounded, context.t('home.sell'), () {
Navigator.pushNamed(context, '/sell');
}),
_buildQuickAction(context, Icons.check_circle_outline_rounded, context.t('home.redeem'), () {
Navigator.pushNamed(context, '/redeem');
}),
],
),
],
),
),
);
}
Widget _buildQuickAction(
BuildContext context, IconData icon, String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: AppSpacing.borderRadiusMd,
),
child: Icon(icon, size: 20, color: Colors.white),
),
const SizedBox(height: 6),
Text(
label,
style: AppTypography.caption.copyWith(
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w500,
),
),
],
),
);
}
// ============================================================
// Category Grid (8 items, 4x2, A+C savings-focused)
// ============================================================
Widget _buildCategoryGrid(BuildContext context) {
final categories = [
(context.t('home.flashSale'), Icons.flash_on_rounded, AppColors.error),
(context.t('home.newRelease'), Icons.fiber_new_rounded, AppColors.primary),
(context.t('home.discountRank'), Icons.trending_up_rounded, AppColors.couponEntertainment),
(context.t('home.expiringSoon'), Icons.timer_rounded, AppColors.warning),
(context.t('home.priceCompare'), Icons.compare_arrows_rounded, AppColors.couponShopping),
(context.t('home.resaleMarket'), Icons.swap_horiz_rounded, AppColors.info),
(context.t('home.hotTrades'), Icons.local_fire_department_rounded, AppColors.couponDining),
(context.t('home.viewAll'), Icons.grid_view_rounded, AppColors.textSecondary),
];
return Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 0.9,
),
itemCount: categories.length,
itemBuilder: (context, index) {
final (name, icon, color) = categories[index];
return GestureDetector(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: AppSpacing.borderRadiusMd,
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(height: 6),
Text(name, style: AppTypography.caption.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w500,
)),
],
),
);
},
),
);
}
Widget _buildAiSuggestions(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(20, 16, 20, 0),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: AppSpacing.borderRadiusSm,
),
child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.t('home.aiRecommend'), style: AppTypography.labelSmall.copyWith(
color: AppColors.primary,
)),
const SizedBox(height: 2),
Text(
context.t('home.aiRecommendDesc'),
style: AppTypography.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 20),
],
),
);
}
}